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}