ferrous_di/provider/
scope.rs

1//! Scoped service resolution and lifecycle management.
2//!
3//! This module contains the Scope and ScopedResolver types for managing
4//! request-scoped services and their automatic disposal.
5
6#[cfg(not(feature = "once-cell"))]
7use std::collections::HashMap;
8use std::sync::{Arc, Mutex};
9use std::future::Future;
10
11#[cfg(feature = "once-cell")]
12use once_cell::sync::OnceCell;
13
14use crate::{DiResult, DiError, Key, Lifetime};
15use crate::registration::AnyArc;
16use super::ResolverContext;
17use crate::internal::{DisposeBag, BoxFutureUnit, with_circular_catch};
18use crate::traits::{Resolver, ResolverCore, Dispose, AsyncDispose};
19use super::ServiceProvider;
20
21/// Scoped service container for request-scoped dependency resolution.
22///
23/// A `Scope` provides isolated dependency resolution for scoped services while
24/// still accessing singleton services from the root provider. This is ideal for
25/// web applications where you want request-scoped services (like database connections,
26/// user contexts, etc.) that are shared within a single request but isolated
27/// between requests.
28///
29/// # Lifetime Behavior
30///
31/// - **Singleton**: Resolved and cached in the root provider (shared across all scopes)
32/// - **Scoped**: Resolved and cached within this specific scope
33/// - **Transient**: Created fresh on every resolution (no caching)
34///
35/// # Examples
36///
37/// ```
38/// use ferrous_di::{ServiceCollection, Resolver};
39/// use std::sync::{Arc, Mutex};
40///
41/// #[derive(Debug)]
42/// struct DatabaseConnection(String);
43///
44/// #[derive(Debug)]
45/// struct UserService {
46///     db: Arc<DatabaseConnection>,
47/// }
48///
49/// let mut collection = ServiceCollection::new();
50///
51/// // Scoped database connection per request
52/// collection.add_scoped_factory::<DatabaseConnection, _>(|_| {
53///     DatabaseConnection("connection-123".to_string())
54/// });
55///
56/// // Transient user service that uses scoped DB connection
57/// collection.add_transient_factory::<UserService, _>(|resolver| {
58///     UserService {
59///         db: resolver.get_required::<DatabaseConnection>(),
60///     }
61/// });
62///
63/// let provider = collection.build();
64/// let scope = provider.create_scope();
65///
66/// // Multiple services in the same scope share the same DB connection
67/// let user1 = scope.get_required::<UserService>();
68/// let user2 = scope.get_required::<UserService>();
69/// assert!(Arc::ptr_eq(&user1.db, &user2.db));
70/// ```
71pub struct Scope {
72    pub(crate) root: ServiceProvider,
73    // Slot-based scoped storage for O(1) access
74    #[cfg(feature = "once-cell")]
75    pub(crate) scoped_cells: Box<[OnceCell<AnyArc>]>,
76    #[cfg(not(feature = "once-cell"))]
77    pub(crate) scoped: Mutex<HashMap<Key, AnyArc>>,
78    pub(crate) scoped_disposers: Mutex<DisposeBag>,
79}
80
81impl Clone for Scope {
82    fn clone(&self) -> Self {
83        // Create a new scope with the same root but fresh scoped state
84        #[cfg(feature = "once-cell")]
85        {
86            let scoped_count = self.root.inner().registry.scoped_count;
87            let scoped_cells: Box<[OnceCell<AnyArc>]> = (0..scoped_count)
88                .map(|_| OnceCell::new())
89                .collect::<Vec<_>>()
90                .into_boxed_slice();
91                
92            Self {
93                root: self.root.clone(),
94                scoped_cells,
95                scoped_disposers: Mutex::new(DisposeBag::default()),
96            }
97        }
98        
99        #[cfg(not(feature = "once-cell"))]
100        {
101            Self {
102                root: self.root.clone(),
103                scoped: Mutex::new(HashMap::new()),
104                scoped_disposers: Mutex::new(DisposeBag::default()),
105            }
106        }
107    }
108}
109
110impl ResolverCore for Scope {
111    fn resolve_any(&self, key: &Key) -> DiResult<AnyArc> {
112        let name = key.display_name();
113        with_circular_catch(name, || self.resolve_any_impl(key))
114    }
115    
116    fn resolve_many(&self, key: &Key) -> DiResult<Vec<AnyArc>> {
117        if let Key::Trait(_trait_name) = key {
118            let name = key.display_name();
119            with_circular_catch(name, || self.resolve_many_impl(key))
120        } else {
121            Ok(Vec::new())
122        }
123    }
124
125    fn push_sync_disposer(&self, f: Box<dyn FnOnce() + Send>) {
126        self.scoped_disposers.lock().unwrap().push_sync(f);
127    }
128
129    fn push_async_disposer(&self, f: Box<dyn FnOnce() -> BoxFutureUnit + Send>) {
130        self.scoped_disposers.lock().unwrap().push_async(move || (f)());
131    }
132}
133
134impl Scope {
135    /// Ultra-optimized scoped resolution using slot-based Vec storage
136    #[inline(always)]
137    fn resolve_scoped(&self, reg: &crate::registration::Registration, _key: &Key) -> DiResult<AnyArc> {
138        #[cfg(feature = "once-cell")]
139        {
140            if let Some(slot) = reg.scoped_slot {
141                let cell = &self.scoped_cells[slot];
142                
143                // Ultra-fast path: check if already initialized
144                if let Some(value) = cell.get() {
145                    return Ok(value.clone());
146                }
147                
148                // Slow path: initialize with factory (unlikely after first access)
149                // TODO: Add std::hint::unlikely when stable
150                {
151                    let ctx = ResolverContext::new(self);
152                    let v = (reg.ctor)(&ctx)?;
153                    let stored = cell.get_or_init(|| v.clone()).clone();
154                    return Ok(stored);
155                }
156            }
157        }
158        
159        #[cfg(not(feature = "once-cell"))]
160        {
161            // Use HashMap for scoped caching when once-cell is not available
162            let key = _key.clone();
163            
164            // Check if already cached
165            {
166                let guard = self.scoped.lock().unwrap();
167                if let Some(cached) = guard.get(&key) {
168                    return Ok(cached.clone());
169                }
170            }
171            
172            // Create and cache the value
173            let ctx = ResolverContext::new(self);
174            let value = (reg.ctor)(&ctx)?;
175            
176            // Cache the value
177            {
178                let mut guard = self.scoped.lock().unwrap();
179                guard.insert(key, value.clone());
180            }
181            
182            Ok(value)
183        }
184        
185        #[cfg(feature = "once-cell")]
186        {
187            // Fallback if no slot assigned (shouldn't happen with once-cell)
188            let ctx = ResolverContext::new(self);
189            (reg.ctor)(&ctx)
190        }
191    }
192
193    fn resolve_any_impl(&self, key: &Key) -> DiResult<AnyArc> {
194        let name = key.display_name();
195        
196        if let Some(reg) = self.root.inner().registry.get(key) {
197            match reg.lifetime {
198                Lifetime::Singleton => {
199                    // Delegate to root provider's optimized singleton resolution
200                    self.root.resolve_singleton(reg, key)
201                }
202                Lifetime::Scoped => {
203                    // Use optimized slot-based scoped resolution
204                    self.resolve_scoped(reg, key)
205                }
206                Lifetime::Transient => {
207                    let ctx = ResolverContext::new(self);
208                    (reg.ctor)(&ctx)  // CRITICAL FIX: pass self (scope) as resolver
209                }
210            }
211        } else if let Key::Trait(trait_name) = key {
212            // Fallback: if trait has multi-bindings, return last as single
213            if let Some(regs) = self.root.inner().registry.many.get(trait_name) {
214                if let Some(last) = regs.last() {
215                    let ctx = ResolverContext::new(self);
216                    (last.ctor)(&ctx)  // CRITICAL FIX: pass self (scope) as resolver
217                } else {
218                    Err(DiError::NotFound(name))
219                }
220            } else {
221                Err(DiError::NotFound(name))
222            }
223        } else {
224            Err(DiError::NotFound(name))
225        }
226    }
227    
228    fn resolve_many_impl(&self, key: &Key) -> DiResult<Vec<AnyArc>> {
229        if let Key::Trait(trait_name) = key {
230            
231            if let Some(regs) = self.root.inner().registry.many.get(trait_name) {
232                let mut results = Vec::with_capacity(regs.len());
233                
234                for (i, reg) in regs.iter().enumerate() {
235                    let multi_key = Key::MultiTrait(trait_name, i);
236                    
237                    let value = match reg.lifetime {
238                        Lifetime::Singleton => {
239                            // Expert fix: Double-checked locking for singletons
240                            {
241                                let cache = self.root.inner().singletons.lock().unwrap();
242                                if let Some(cached) = cache.get(&multi_key) {
243                                    results.push(cached.clone());
244                                    continue;
245                                }
246                            } // Lock released here
247                            
248                            // Create without holding lock
249                            let ctx = ResolverContext::new(self);
250                            let value = (reg.ctor)(&ctx)?;
251                            
252                            // Double-checked insert
253                            {
254                                let mut cache = self.root.inner().singletons.lock().unwrap();
255                                if let Some(cached) = cache.get(&multi_key) {
256                                    cached.clone() // Another thread beat us
257                                } else {
258                                    cache.insert(multi_key, value.clone());
259                                    value
260                                }
261                            }
262                        }
263                        Lifetime::Scoped => {
264                            // Use slot-based scoped resolution for multi-bindings
265                            #[allow(unused_variables)]
266                            if let Some(slot) = reg.scoped_slot {
267                                #[cfg(feature = "once-cell")]
268                                {
269                                    let cell = &self.scoped_cells[slot];
270                                    
271                                    // Ultra-fast path: check if already initialized
272                                    if let Some(value) = cell.get() {
273                                        value.clone()
274                                    } else {
275                                        // Slow path: initialize with factory
276                                        let ctx = ResolverContext::new(self);
277                                        let v = (reg.ctor)(&ctx)?;
278                                        cell.get_or_init(|| v.clone()).clone()
279                                    }
280                                }
281                                #[cfg(not(feature = "once-cell"))]
282                                {
283                                    // Use HashMap caching for scoped multi-bindings when once-cell is not available
284                                    let multi_key = Key::MultiTrait(trait_name, i);
285                                    
286                                    // Check if already cached
287                                    {
288                                        let guard = self.scoped.lock().unwrap();
289                                        if let Some(cached) = guard.get(&multi_key) {
290                                            cached.clone()
291                                        } else {
292                                            drop(guard); // Release lock before creating
293                                            
294                                            // Create and cache the value
295                                            let ctx = ResolverContext::new(self);
296                                            let value = (reg.ctor)(&ctx)?;
297                                            
298                                            let mut guard = self.scoped.lock().unwrap();
299                                            guard.insert(multi_key, value.clone());
300                                            value
301                                        }
302                                    }
303                                }
304                            } else {
305                                // No slot assigned - fallback to transient behavior
306                                let ctx = ResolverContext::new(self);
307                                (reg.ctor)(&ctx)?
308                            }
309                        }
310                        Lifetime::Transient => {
311                            let ctx = ResolverContext::new(self);
312                            (reg.ctor)(&ctx)?
313                        }
314                    };
315                    
316                    results.push(value);
317                }
318                
319                Ok(results)
320            } else {
321                Ok(Vec::new())
322            }
323        } else {
324            Ok(Vec::new())
325        }
326    }
327
328    /// Disposes all scoped disposal hooks in LIFO order.
329    ///
330    /// This method runs all asynchronous disposal hooks first (in reverse order),
331    /// followed by all synchronous disposal hooks (in reverse order). This ensures
332    /// proper cleanup of scoped services.
333    ///
334    /// # Examples
335    ///
336    /// ```
337    /// use ferrous_di::{ServiceCollection, Dispose, Resolver};
338    /// use std::sync::Arc;
339    ///
340    /// struct ScopedCache {
341    ///     name: String,
342    /// }
343    ///
344    /// impl Dispose for ScopedCache {
345    ///     fn dispose(&self) {
346    ///         println!("Disposing scoped cache: {}", self.name);
347    ///     }
348    /// }
349    ///
350    /// # async fn example() {
351    /// let mut services = ServiceCollection::new();
352    /// services.add_scoped_factory::<ScopedCache, _>(|r| {
353    ///     let cache = Arc::new(ScopedCache { name: "request_cache".to_string() });
354    ///     r.register_disposer(cache.clone());
355    ///     ScopedCache { name: "request_cache".to_string() } // Return concrete type
356    /// });
357    ///
358    /// let provider = services.build();
359    /// let scope = provider.create_scope();
360    /// // ... use scoped services ...
361    /// scope.dispose_all().await; // Only disposes scoped resources
362    /// # }
363    /// ```
364    pub async fn dispose_all(&self) {
365        // First run async disposers in reverse order
366        self.scoped_disposers.lock().unwrap().run_all_async_reverse().await;
367        // Then run sync disposers in reverse order  
368        self.scoped_disposers.lock().unwrap().run_all_sync_reverse();
369    }
370
371    /// Executes an async block with automatic disposal of services resolved via `*_disposable` methods.
372    ///
373    /// This method provides a "using" pattern where services resolved with the disposable
374    /// variants (`get_disposable`, `get_async_disposable`, etc.) are automatically disposed
375    /// when the block exits, regardless of whether it succeeds or fails.
376    ///
377    /// # Disposal Order
378    ///
379    /// Services are disposed in LIFO order (last resolved, first disposed):
380    /// 1. Async disposers run first (in reverse order)
381    /// 2. Sync disposers run second (in reverse order)
382    ///
383    /// # Error Handling
384    ///
385    /// The block's result is preserved even if disposal occurs. Disposal happens
386    /// regardless of success or failure of the user block.
387    ///
388    /// # Examples
389    ///
390    /// ```
391    /// use ferrous_di::{ServiceCollection, Dispose, AsyncDispose, DiError};
392    /// use async_trait::async_trait;
393    /// use std::sync::Arc;
394    ///
395    /// struct DatabaseConnection;
396    /// impl Dispose for DatabaseConnection {
397    ///     fn dispose(&self) {
398    ///         println!("Closing database connection");
399    ///     }
400    /// }
401    ///
402    /// struct ApiClient;
403    /// #[async_trait]
404    /// impl AsyncDispose for ApiClient {
405    ///     async fn dispose(&self) {
406    ///         println!("Shutting down API client");
407    ///     }
408    /// }
409    ///
410    /// # async fn example() -> Result<(), DiError> {
411    /// let mut services = ServiceCollection::new();
412    /// services.add_scoped_factory::<DatabaseConnection, _>(|_| DatabaseConnection);
413    /// services.add_scoped_factory::<ApiClient, _>(|_| ApiClient);
414    ///
415    /// let provider = services.build();
416    /// let scope = provider.create_scope();
417    ///
418    /// let result = scope.using(|resolver| async move {
419    ///     let db = resolver.get_disposable::<DatabaseConnection>()?;
420    ///     let api = resolver.get_async_disposable::<ApiClient>()?;
421    ///     
422    ///     // Use the services...
423    ///     Ok::<String, DiError>("Operation completed".to_string())
424    /// }).await?;
425    ///
426    /// // Both services are automatically disposed here in LIFO order:
427    /// // 1. ApiClient.dispose() (async)  
428    /// // 2. DatabaseConnection.dispose() (sync)
429    ///
430    /// assert_eq!(result, "Operation completed");
431    /// # Ok(())
432    /// # }
433    /// ```
434    pub async fn using<F, Fut, R, E>(&self, f: F) -> Result<R, E>
435    where
436        F: FnOnce(ScopedResolver) -> Fut,
437        Fut: Future<Output = Result<R, E>>,
438        E: From<DiError>,
439    {
440        let resolver = ScopedResolver::new(self);
441        let bag_handle = resolver.bag.clone();
442
443        // Run user code
444        let result = f(resolver).await;
445
446        // Always dispose (even on error): async then sync, LIFO
447        let mut bag = std::mem::take(&mut *bag_handle.lock().unwrap());
448        bag.run_all_async_reverse().await;
449        bag.run_all_sync_reverse();
450
451        result
452    }
453
454    /// Executes a synchronous block with automatic disposal of services resolved via `*_disposable` methods.
455    ///
456    /// This is the synchronous variant of [`using`](Self::using) for blocks that don't need async.
457    /// Only synchronous disposers are supported - async disposers will be ignored in this method.
458    ///
459    /// # Examples
460    ///
461    /// ```
462    /// use ferrous_di::{ServiceCollection, Dispose, DiError};
463    /// use std::sync::Arc;
464    ///
465    /// struct FileHandle;
466    /// impl Dispose for FileHandle {
467    ///     fn dispose(&self) {
468    ///         println!("Closing file");
469    ///     }
470    /// }
471    ///
472    /// # fn example() -> Result<(), DiError> {
473    /// let mut services = ServiceCollection::new();
474    /// services.add_scoped_factory::<FileHandle, _>(|_| FileHandle);
475    ///
476    /// let provider = services.build();
477    /// let scope = provider.create_scope();
478    ///
479    /// let result = scope.using_sync(|resolver| {
480    ///     let file = resolver.get_disposable::<FileHandle>()?;
481    ///     // Use the file...
482    ///     Ok::<String, DiError>("File processed".to_string())
483    /// })?;
484    ///
485    /// // FileHandle is automatically disposed here
486    /// assert_eq!(result, "File processed");
487    /// # Ok(())
488    /// # }
489    /// ```
490    pub fn using_sync<F, R, E>(&self, f: F) -> Result<R, E>
491    where
492        F: FnOnce(ScopedResolver) -> Result<R, E>,
493        E: From<DiError>,
494    {
495        let resolver = ScopedResolver::new(self);
496        let bag_handle = resolver.bag.clone();
497
498        let result = f(resolver);
499
500        let mut bag = std::mem::take(&mut *bag_handle.lock().unwrap());
501        bag.run_all_sync_reverse();
502
503        result
504    }
505
506    /// Creates a child scope with fresh scoped state.
507    ///
508    /// Used by labeled scopes for hierarchical scope management in workflow engines.
509    /// The child scope inherits the same root ServiceProvider but has independent scoped storage.
510    pub fn create_child(&self) -> Self {
511        self.clone()
512    }
513}
514
515impl Drop for Scope {
516    fn drop(&mut self) {
517        // Check if there are undisposed scoped resources and warn
518        let bag = self.scoped_disposers.get_mut().unwrap();
519        if !bag.is_empty() {
520            eprintln!("[ferrous-di] Scope dropped with undisposed resources. Call dispose_all().await before dropping.");
521        }
522    }
523}
524
525impl Resolver for Scope {
526    fn register_disposer<T>(&self, service: Arc<T>)
527    where
528        T: Dispose + 'static,
529    {
530        self.push_sync_disposer(Box::new(move || service.dispose()));
531    }
532
533    fn register_async_disposer<T>(&self, service: Arc<T>)
534    where
535        T: AsyncDispose + 'static,
536    {
537        self.push_async_disposer(Box::new(move || {
538            let service = service.clone();
539            Box::pin(async move { service.dispose().await })
540        }));
541    }
542}
543
544// ===== ScopedResolver =====
545
546/// Block-scoped resolver with automatic disposal of requested services.
547///
548/// `ScopedResolver` provides automatic disposal registration for services resolved
549/// within a `using()` block. It maintains a shared `DisposeBag` that is automatically
550/// disposed at the end of the block in LIFO order (async disposers first, then sync).
551///
552/// The resolver is cloneable and can be safely moved into async closures thanks to
553/// its shared interior state.
554///
555/// # Usage
556///
557/// Use the `*_disposable` methods to resolve services that should be automatically
558/// disposed when the block exits. Regular `get*` methods work normally without
559/// auto-disposal registration.
560///
561/// # Examples
562///
563/// ```
564/// use ferrous_di::{ServiceCollection, Dispose, AsyncDispose};
565/// use async_trait::async_trait;
566/// use std::sync::Arc;
567///
568/// struct DbConnection;
569/// impl Dispose for DbConnection {
570///     fn dispose(&self) {
571///         // Close database connection
572///     }
573/// }
574///
575/// struct ApiClient;
576/// #[async_trait]
577/// impl AsyncDispose for ApiClient {
578///     async fn dispose(&self) {
579///         // Graceful shutdown
580///     }
581/// }
582///
583/// # async fn example() -> Result<(), ferrous_di::DiError> {
584/// let mut services = ServiceCollection::new();
585/// services.add_scoped_factory::<DbConnection, _>(|_| DbConnection);
586/// services.add_scoped_factory::<ApiClient, _>(|_| ApiClient);
587///
588/// let provider = services.build();
589/// let scope = provider.create_scope();
590///
591/// let result = scope.using(|resolver| async move {
592///     let db = resolver.get_disposable::<DbConnection>()?; // Auto-disposed
593///     let api = resolver.get_async_disposable::<ApiClient>()?; // Auto-disposed
594///     // ... use services ...
595///     Ok::<i32, ferrous_di::DiError>(42)
596/// }).await?;
597/// // db and api automatically disposed in LIFO order here
598/// # Ok(())
599/// # }
600/// ```
601#[derive(Clone)]
602pub struct ScopedResolver {
603    scope: Arc<Scope>,
604    // Shared bag so resolver can be moved into async closures safely
605    pub(crate) bag: Arc<Mutex<DisposeBag>>,
606}
607
608impl ScopedResolver {
609    pub(crate) fn new(scope: &Scope) -> Self {
610        Self { 
611            scope: Arc::new(scope.clone()), 
612            bag: Arc::new(Mutex::new(DisposeBag::default()))
613        }
614    }
615
616    // --- Plain resolution (no auto-dispose) ---
617
618    /// Resolves a concrete service type without auto-disposal registration.
619    ///
620    /// This method works exactly like `Scope::get()` and preserves circular
621    /// dependency detection. The service will NOT be automatically disposed.
622    pub fn get<T: 'static + Send + Sync>(&self) -> DiResult<Arc<T>> {
623        self.scope.get::<T>()
624    }
625
626    /// Resolves a single trait implementation without auto-disposal registration.
627    ///
628    /// This method works exactly like `Scope::get_trait()` and preserves circular
629    /// dependency detection. The service will NOT be automatically disposed.
630    pub fn get_trait<T: ?Sized + 'static + Send + Sync>(&self) -> DiResult<Arc<T>> {
631        self.scope.get_trait::<T>()
632    }
633
634    /// Resolves all trait implementations without auto-disposal registration.
635    ///
636    /// This method works exactly like `Scope::get_all_trait()`. The services
637    /// will NOT be automatically disposed.
638    pub fn get_all_trait<T: ?Sized + 'static + Send + Sync>(&self) -> DiResult<Vec<Arc<T>>> {
639        self.scope.get_all_trait::<T>()
640    }
641
642    // --- Auto-disposing variants for concrete types ---
643
644    /// Resolves a concrete service type and registers it for automatic synchronous disposal.
645    ///
646    /// The service will be disposed when the `using()` block exits, in LIFO order.
647    /// The service must implement the `Dispose` trait.
648    ///
649    /// # Examples
650    ///
651    /// ```
652    /// # use ferrous_di::{ServiceCollection, Dispose};
653    /// # use std::sync::Arc;
654    /// struct Cache;
655    /// impl Dispose for Cache {
656    ///     fn dispose(&self) { /* cleanup */ }
657    /// }
658    ///
659    /// # async fn example() -> Result<(), ferrous_di::DiError> {
660    /// # let mut services = ServiceCollection::new();
661    /// # services.add_scoped_factory::<Cache, _>(|_| Cache);
662    /// # let provider = services.build();
663    /// # let scope = provider.create_scope();
664    /// scope.using(|resolver| async move {
665    ///     let cache = resolver.get_disposable::<Cache>()?; // Auto-disposed on block exit
666    ///     Ok::<(), ferrous_di::DiError>(())
667    /// }).await?;
668    /// # Ok(())
669    /// # }
670    /// ```
671    pub fn get_disposable<T>(&self) -> DiResult<Arc<T>>
672    where
673        T: Dispose + 'static,
674    {
675        let s = self.scope.get::<T>()?;
676        let clone = s.clone();
677        self.bag.lock().unwrap().push_sync(Box::new(move || clone.dispose()));
678        Ok(s)
679    }
680
681    /// Resolves a concrete service type and registers it for automatic asynchronous disposal.
682    ///
683    /// The service will be disposed when the `using()` block exits, in LIFO order.
684    /// Async disposers run before sync disposers. The service must implement `AsyncDispose`.
685    ///
686    /// # Examples
687    ///
688    /// ```
689    /// # use ferrous_di::{ServiceCollection, AsyncDispose};
690    /// # use async_trait::async_trait;
691    /// # use std::sync::Arc;
692    /// struct ApiClient;
693    /// #[async_trait]
694    /// impl AsyncDispose for ApiClient {
695    ///     async fn dispose(&self) { /* async cleanup */ }
696    /// }
697    ///
698    /// # async fn example() -> Result<(), ferrous_di::DiError> {
699    /// # let mut services = ServiceCollection::new();
700    /// # services.add_scoped_factory::<ApiClient, _>(|_| ApiClient);
701    /// # let provider = services.build();
702    /// # let scope = provider.create_scope();
703    /// scope.using(|resolver| async move {
704    ///     let client = resolver.get_async_disposable::<ApiClient>()?;
705    ///     Ok::<(), ferrous_di::DiError>(())
706    /// }).await?;
707    /// # Ok(())
708    /// # }
709    /// ```
710    pub fn get_async_disposable<T>(&self) -> DiResult<Arc<T>>
711    where
712        T: AsyncDispose + 'static,
713    {
714        let s = self.scope.get::<T>()?;
715        let clone = s.clone();
716        self.bag.lock().unwrap().push_async(move || async move { clone.dispose().await });
717        Ok(s)
718    }
719
720    // --- Auto-disposing variants for trait objects ---
721
722    /// Resolves a trait implementation and registers it for automatic synchronous disposal.
723    ///
724    /// The trait object will be disposed when the `using()` block exits, in LIFO order.
725    /// The trait must extend `Dispose`.
726    ///
727    /// # Examples
728    ///
729    /// ```
730    /// # use ferrous_di::{ServiceCollection, Dispose};
731    /// # use std::sync::Arc;
732    /// trait Cache: Dispose + Send + Sync {}
733    /// struct MemoryCache;
734    /// impl Dispose for MemoryCache {
735    ///     fn dispose(&self) { /* cleanup */ }
736    /// }
737    /// impl Cache for MemoryCache {}
738    ///
739    /// # async fn example() -> Result<(), ferrous_di::DiError> {
740    /// # let mut services = ServiceCollection::new();
741    /// # services.add_scoped_trait_factory::<dyn Cache, _>(|_| Arc::new(MemoryCache));
742    /// # let provider = services.build();
743    /// # let scope = provider.create_scope();
744    /// scope.using(|resolver| async move {
745    ///     let cache = resolver.get_trait_disposable::<dyn Cache>()?;
746    ///     Ok::<(), ferrous_di::DiError>(())
747    /// }).await?;
748    /// # Ok(())
749    /// # }
750    /// ```
751    pub fn get_trait_disposable<T>(&self) -> DiResult<Arc<T>>
752    where
753        T: ?Sized + Dispose + 'static + Send + Sync,
754    {
755        let s = self.scope.get_trait::<T>()?;
756        let clone = s.clone();
757        self.bag.lock().unwrap().push_sync(Box::new(move || clone.dispose()));
758        Ok(s)
759    }
760
761    /// Resolves a trait implementation and registers it for automatic asynchronous disposal.
762    ///
763    /// The trait object will be disposed when the `using()` block exits, in LIFO order.
764    /// Async disposers run before sync disposers. The trait must extend `AsyncDispose`.
765    ///
766    /// # Examples
767    ///
768    /// ```
769    /// # use ferrous_di::{ServiceCollection, AsyncDispose};
770    /// # use async_trait::async_trait;
771    /// # use std::sync::Arc;
772    /// #[async_trait]
773    /// trait ApiClient: AsyncDispose + Send + Sync {
774    ///     async fn call_api(&self) -> String;
775    /// }
776    ///
777    /// struct HttpClient;
778    /// #[async_trait]
779    /// impl AsyncDispose for HttpClient {
780    ///     async fn dispose(&self) { /* cleanup */ }
781    /// }
782    /// #[async_trait]
783    /// impl ApiClient for HttpClient {
784    ///     async fn call_api(&self) -> String { "response".to_string() }
785    /// }
786    ///
787    /// # async fn example() -> Result<(), ferrous_di::DiError> {
788    /// # let mut services = ServiceCollection::new();
789    /// # services.add_scoped_trait_factory::<dyn ApiClient, _>(|_| Arc::new(HttpClient));
790    /// # let provider = services.build();
791    /// # let scope = provider.create_scope();
792    /// scope.using(|resolver| async move {
793    ///     let client = resolver.get_trait_async_disposable::<dyn ApiClient>()?;
794    ///     let response = client.call_api().await;
795    ///     Ok::<String, ferrous_di::DiError>(response)
796    /// }).await?;
797    /// # Ok(())
798    /// # }
799    /// ```
800    pub fn get_trait_async_disposable<T>(&self) -> DiResult<Arc<T>>
801    where
802        T: ?Sized + AsyncDispose + 'static + Send + Sync,
803    {
804        let s = self.scope.get_trait::<T>()?;
805        let clone = s.clone();
806        self.bag.lock().unwrap().push_async(move || async move { clone.dispose().await });
807        Ok(s)
808    }
809}