diff --git a/benchmark_di/pubspec.lock b/benchmark_di/pubspec.lock index f3d4d99..b523a47 100644 --- a/benchmark_di/pubspec.lock +++ b/benchmark_di/pubspec.lock @@ -47,7 +47,7 @@ packages: path: "../cherrypick" relative: true source: path - version: "3.0.0-dev.7" + version: "3.0.0-dev.8" collection: dependency: transitive description: diff --git a/cherrypick/lib/src/binding.dart b/cherrypick/lib/src/binding.dart index c43ee3e..3f7cb00 100644 Binary files a/cherrypick/lib/src/binding.dart and b/cherrypick/lib/src/binding.dart differ diff --git a/cherrypick/lib/src/binding_resolver.dart b/cherrypick/lib/src/binding_resolver.dart index 708ec1d..0689933 100644 --- a/cherrypick/lib/src/binding_resolver.dart +++ b/cherrypick/lib/src/binding_resolver.dart @@ -13,41 +13,70 @@ import 'dart:async'; +/// Represents a direct instance or an async instance ([T] or [Future]). +/// Used for both direct and async bindings. +/// +/// Example: +/// ```dart +/// Instance sync = "hello"; +/// Instance async = Future.value(MyApi()); +/// ``` typedef Instance = FutureOr; -/// RU: Синхронный или асинхронный провайдер без параметров, возвращающий [T] или [Future]. -/// ENG: Synchronous or asynchronous provider without parameters, returning [T] or [Future]. +/// Provider function type for synchronous or asynchronous, parameterless creation of [T]. +/// Can return [T] or [Future]. +/// +/// Example: +/// ```dart +/// Provider provider = () => MyService(); +/// Provider asyncProvider = () async => await Api.connect(); +/// ``` typedef Provider = FutureOr Function(); -/// RU: Провайдер с динамическим параметром, возвращающий [T] или [Future] в зависимости от реализации. -/// ENG: Provider with dynamic parameter, returning [T] or [Future] depending on implementation. +/// Provider function type that accepts a dynamic parameter, for factory/parametrized injection. +/// Returns [T] or [Future]. +/// +/// Example: +/// ```dart +/// ProviderWithParams provider = (params) => User(params["name"]); +/// ``` typedef ProviderWithParams = FutureOr Function(dynamic); -/// RU: Абстрактный интерфейс для классов, которые разрешают зависимости типа [T]. -/// ENG: Abstract interface for classes that resolve dependencies of type [T]. +/// Abstract interface for dependency resolvers used by [Binding]. +/// Defines how to resolve instances of type [T]. +/// +/// You usually don't use this directly; it's used internally for advanced/low-level DI. abstract class BindingResolver { - /// RU: Синхронное разрешение зависимости с параметром [params]. - /// ENG: Synchronous resolution of the dependency with [params]. + /// Synchronously resolves the dependency, optionally taking parameters (for factory cases). + /// Throws if implementation does not support sync resolution. T? resolveSync([dynamic params]); - /// RU: Асинхронное разрешение зависимости с параметром [params]. - /// ENG: Asynchronous resolution of the dependency with [params]. + /// Asynchronously resolves the dependency, optionally taking parameters (for factory cases). + /// If instance is already a [Future], returns it directly. Future? resolveAsync([dynamic params]); - /// RU: Помечает текущий резолвер как синглтон — результат будет закеширован. - /// ENG: Marks this resolver as singleton — result will be cached. + /// Marks this resolver as singleton: instance(s) will be cached and reused inside the scope. void toSingleton(); + /// Returns true if this resolver is marked as singleton. bool get isSingleton; } -/// RU: Резолвер, оборачивающий конкретный экземпляр [T] (или Future), без вызова провайдера. -/// ENG: Resolver that wraps a concrete instance of [T] (or Future), without provider invocation. +/// Concrete resolver for direct instance ([T] or [Future]). No provider is called. +/// +/// Used for [Binding.toInstance]. +/// Supports both sync and async resolution; sync will throw if underlying instance is [Future]. +/// Examples: +/// ```dart +/// var resolver = InstanceResolver("hello"); +/// resolver.resolveSync(); // == "hello" +/// var asyncResolver = InstanceResolver(Future.value(7)); +/// asyncResolver.resolveAsync(); // Future +/// ``` class InstanceResolver implements BindingResolver { final Instance _instance; - /// RU: Создаёт резолвер, оборачивающий значение [instance]. - /// ENG: Creates a resolver that wraps the given [instance]. + /// Wraps the given instance (sync or async) in a resolver. InstanceResolver(this._instance); @override @@ -62,7 +91,6 @@ class InstanceResolver implements BindingResolver { @override Future resolveAsync([_]) { if (_instance is Future) return _instance; - return Future.value(_instance); } @@ -73,8 +101,23 @@ class InstanceResolver implements BindingResolver { bool get isSingleton => true; } -/// RU: Резолвер, оборачивающий провайдер, с возможностью синглтон-кеширования. -/// ENG: Resolver that wraps a provider, with optional singleton caching. +/// Resolver for provider functions (sync/async/factory), with optional singleton caching. +/// Used for [Binding.toProvide], [Binding.toProvideWithParams], [Binding.singleton]. +/// +/// Examples: +/// ```dart +/// // No param, sync: +/// var r = ProviderResolver((_) => 5, withParams: false); +/// r.resolveSync(); // == 5 +/// // With param: +/// var rp = ProviderResolver((p) => p * 2, withParams: true); +/// rp.resolveSync(2); // == 4 +/// // Singleton: +/// r.toSingleton(); +/// // Async: +/// var ra = ProviderResolver((_) async => await Future.value(10), withParams: false); +/// await ra.resolveAsync(); // == 10 +/// ``` class ProviderResolver implements BindingResolver { final ProviderWithParams _provider; final bool _withParams; @@ -82,8 +125,7 @@ class ProviderResolver implements BindingResolver { FutureOr? _cache; bool _singleton = false; - /// RU: Создаёт резолвер из произвольной функции [raw], поддерживающей ноль или один параметр. - /// ENG: Creates a resolver from arbitrary function [raw], supporting zero or one parameter. + /// Creates a resolver from [provider], optionally accepting dynamic params. ProviderResolver( ProviderWithParams provider, { required bool withParams, @@ -93,16 +135,13 @@ class ProviderResolver implements BindingResolver { @override T resolveSync([dynamic params]) { _checkParams(params); - final result = _cache ?? _provider(params); - if (result is T) { if (_singleton) { _cache ??= result; } return result; } - throw StateError( 'Provider [$_provider] return Future<$T>. Use resolveAsync() instead.', ); @@ -111,14 +150,11 @@ class ProviderResolver implements BindingResolver { @override Future resolveAsync([dynamic params]) { _checkParams(params); - final result = _cache ?? _provider(params); final target = result is Future ? result : Future.value(result); - if (_singleton) { _cache ??= target; } - return target; } @@ -130,8 +166,7 @@ class ProviderResolver implements BindingResolver { @override bool get isSingleton => _singleton; - /// RU: Проверяет, был ли передан параметр, если провайдер требует его. - /// ENG: Checks if parameter is passed when the provider expects it. + /// Throws if params required but not supplied. void _checkParams(dynamic params) { if (_withParams && params == null) { throw StateError( diff --git a/cherrypick/lib/src/cycle_detector.dart b/cherrypick/lib/src/cycle_detector.dart index ecd5d8e..8106675 100644 --- a/cherrypick/lib/src/cycle_detector.dart +++ b/cherrypick/lib/src/cycle_detector.dart @@ -14,16 +14,20 @@ import 'dart:collection'; import 'package:cherrypick/src/observer.dart'; -/// RU: Исключение, выбрасываемое при обнаружении циклической зависимости. -/// ENG: Exception thrown when a circular dependency is detected. +/// Exception thrown when a circular dependency is detected during dependency resolution. +/// +/// Contains a [message] and the [dependencyChain] showing the resolution cycle. +/// +/// Example diagnostic: +/// ``` +/// CircularDependencyException: Circular dependency detected for A +/// Dependency chain: A -> B -> C -> A +/// ``` class CircularDependencyException implements Exception { final String message; final List dependencyChain; - CircularDependencyException(this.message, this.dependencyChain) { - // DEBUG - - } + CircularDependencyException(this.message, this.dependencyChain); @override String toString() { @@ -32,8 +36,26 @@ class CircularDependencyException implements Exception { } } -/// RU: Детектор циклических зависимостей для CherryPick DI контейнера. -/// ENG: Circular dependency detector for CherryPick DI container. +/// Circular dependency detector for CherryPick DI containers. +/// +/// Tracks dependency resolution chains to detect and prevent infinite recursion caused by cycles. +/// Whenever a resolve chain re-enters a started dependency, a [CircularDependencyException] is thrown with the full chain. +/// +/// This class is used internally, but you can interact with it through [CycleDetectionMixin]. +/// +/// Example usage (pseudocode): +/// ```dart +/// final detector = CycleDetector(observer: myObserver); +/// try { +/// detector.startResolving(); +/// // ... resolving A which depends on B, etc +/// detector.startResolving(); +/// detector.startResolving(); // BOOM: throws exception +/// } finally { +/// detector.finishResolving(); +/// detector.finishResolving(); +/// } +/// ``` class CycleDetector { final CherryPickObserver _observer; final Set _resolutionStack = HashSet(); @@ -41,10 +63,9 @@ class CycleDetector { CycleDetector({required CherryPickObserver observer}) : _observer = observer; - /// RU: Начинает отслеживание разрешения зависимости. - /// ENG: Starts tracking dependency resolution. - /// - /// Throws [CircularDependencyException] if circular dependency is detected. + /// Starts tracking dependency resolution for type [T] and optional [named] qualifier. + /// + /// Throws [CircularDependencyException] if a cycle is found. void startResolving({String? named}) { final dependencyKey = _createDependencyKey(named); _observer.onDiagnostic( @@ -57,26 +78,19 @@ class CycleDetector { if (_resolutionStack.contains(dependencyKey)) { final cycleStartIndex = _resolutionHistory.indexOf(dependencyKey); final cycle = _resolutionHistory.sublist(cycleStartIndex)..add(dependencyKey); - _observer.onCycleDetected( - cycle, - ); - _observer.onError( - 'Cycle detected for $dependencyKey', - null, - null, - ); + _observer.onCycleDetected(cycle); + _observer.onError('Cycle detected for $dependencyKey', null, null); throw CircularDependencyException( 'Circular dependency detected for $dependencyKey', cycle, ); } - _resolutionStack.add(dependencyKey); _resolutionHistory.add(dependencyKey); } - /// RU: Завершает отслеживание разрешения зависимости. - /// ENG: Finishes tracking dependency resolution. + /// Stops tracking dependency resolution for type [T] and optional [named] qualifier. + /// Should always be called after [startResolving], including for errors. void finishResolving({String? named}) { final dependencyKey = _createDependencyKey(named); _observer.onDiagnostic( @@ -84,15 +98,13 @@ class CycleDetector { details: {'event': 'finishResolving'}, ); _resolutionStack.remove(dependencyKey); - // Удаляем из истории только если это последний элемент - if (_resolutionHistory.isNotEmpty && - _resolutionHistory.last == dependencyKey) { + // Only remove from history if it's the last one + if (_resolutionHistory.isNotEmpty && _resolutionHistory.last == dependencyKey) { _resolutionHistory.removeLast(); } } - /// RU: Очищает все состояние детектора. - /// ENG: Clears all detector state. + /// Clears all resolution state and resets the cycle detector. void clear() { _observer.onDiagnostic( 'CycleDetector clear', @@ -105,33 +117,45 @@ class CycleDetector { _resolutionHistory.clear(); } - /// RU: Проверяет, находится ли зависимость в процессе разрешения. - /// ENG: Checks if dependency is currently being resolved. + /// Returns true if dependency [T] (and [named], if specified) is being resolved right now. bool isResolving({String? named}) { final dependencyKey = _createDependencyKey(named); return _resolutionStack.contains(dependencyKey); } - /// RU: Возвращает текущую цепочку разрешения зависимостей. - /// ENG: Returns current dependency resolution chain. + /// Gets the current dependency resolution chain (for diagnostics or debugging). List get currentResolutionChain => List.unmodifiable(_resolutionHistory); - /// RU: Создает уникальный ключ для зависимости. - /// ENG: Creates unique key for dependency. + /// Returns a unique string key for type [T] (+name). String _createDependencyKey(String? named) { final typeName = T.toString(); return named != null ? '$typeName@$named' : typeName; } } -/// RU: Миксин для добавления поддержки обнаружения циклических зависимостей. -/// ENG: Mixin for adding circular dependency detection support. +/// Mixin for adding circular dependency detection support to custom DI containers/classes. +/// +/// Fields: +/// - `observer`: must be implemented by your class (used for diagnostics and error reporting) +/// +/// Example usage: +/// ```dart +/// class MyContainer with CycleDetectionMixin { +/// @override +/// CherryPickObserver get observer => myObserver; +/// } +/// +/// final c = MyContainer(); +/// c.enableCycleDetection(); +/// c.withCycleDetection(String, null, () { +/// // ... dependency resolution code +/// }); +/// ``` mixin CycleDetectionMixin { CycleDetector? _cycleDetector; CherryPickObserver get observer; - /// RU: Включает обнаружение циклических зависимостей. - /// ENG: Enables circular dependency detection. + /// Turns on circular dependency detection for this class/container. void enableCycleDetection() { _cycleDetector = CycleDetector(observer: observer); observer.onDiagnostic( @@ -143,8 +167,7 @@ mixin CycleDetectionMixin { ); } - /// RU: Отключает обнаружение циклических зависимостей. - /// ENG: Disables circular dependency detection. + /// Shuts off detection and clears any cycle history for this container. void disableCycleDetection() { _cycleDetector?.clear(); observer.onDiagnostic( @@ -157,12 +180,17 @@ mixin CycleDetectionMixin { _cycleDetector = null; } - /// RU: Проверяет, включено ли обнаружение циклических зависимостей. - /// ENG: Checks if circular dependency detection is enabled. + /// Returns true if detection is currently enabled. bool get isCycleDetectionEnabled => _cycleDetector != null; - /// RU: Выполняет действие с отслеживанием циклических зависимостей. - /// ENG: Executes action with circular dependency tracking. + /// Executes [action] while tracking for circular DI cycles for [dependencyType] and [named]. + /// + /// Throws [CircularDependencyException] if a dependency cycle is detected. + /// + /// Example: + /// ```dart + /// withCycleDetection(String, 'api', () => resolveApi()); + /// ``` T withCycleDetection( Type dependencyType, String? named, @@ -180,14 +208,8 @@ mixin CycleDetectionMixin { final cycleStartIndex = _cycleDetector!._resolutionHistory.indexOf(dependencyKey); final cycle = _cycleDetector!._resolutionHistory.sublist(cycleStartIndex) ..add(dependencyKey); - observer.onCycleDetected( - cycle, - ); - observer.onError( - 'Cycle detected for $dependencyKey', - null, - null, - ); + observer.onCycleDetected(cycle); + observer.onError('Cycle detected for $dependencyKey', null, null); throw CircularDependencyException( 'Circular dependency detected for $dependencyKey', cycle, @@ -208,8 +230,7 @@ mixin CycleDetectionMixin { } } - /// RU: Возвращает текущую цепочку разрешения зависимостей. - /// ENG: Returns current dependency resolution chain. + /// Gets the current active dependency resolution chain. List get currentResolutionChain => _cycleDetector?.currentResolutionChain ?? []; } diff --git a/cherrypick/lib/src/factory.dart b/cherrypick/lib/src/factory.dart index 9169719..79521c2 100644 --- a/cherrypick/lib/src/factory.dart +++ b/cherrypick/lib/src/factory.dart @@ -12,6 +12,28 @@ // import 'package:cherrypick/src/scope.dart'; +/// Abstract factory interface for creating objects of type [T] using a [Scope]. +/// +/// Can be implemented for advanced dependency injection scenarios where +/// the resolution requires contextual information from the DI [Scope]. +/// +/// Often used to supply complex objects, runtime-bound services, +/// or objects depending on dynamic configuration. +/// +/// Example usage: +/// ```dart +/// class MyServiceFactory implements Factory { +/// @override +/// MyService createInstance(Scope scope) { +/// final db = scope.resolve(named: "main"); +/// return MyService(db); +/// } +/// } +/// +/// // Usage in a module: +/// bind().toProvide(() => MyServiceFactory().createInstance(scope)); +/// ``` abstract class Factory { + /// Implement this to provide an instance of [T], with access to the resolving [scope]. T createInstance(Scope scope); } diff --git a/cherrypick/lib/src/global_cycle_detector.dart b/cherrypick/lib/src/global_cycle_detector.dart index 5bd0e17..47e7ab9 100644 --- a/cherrypick/lib/src/global_cycle_detector.dart +++ b/cherrypick/lib/src/global_cycle_detector.dart @@ -15,33 +15,47 @@ import 'dart:collection'; import 'package:cherrypick/cherrypick.dart'; -/// RU: Глобальный детектор циклических зависимостей для всей иерархии скоупов. -/// ENG: Global circular dependency detector for entire scope hierarchy. +/// GlobalCycleDetector detects and prevents circular dependencies across an entire DI scope hierarchy. +/// +/// This is particularly important for modular/feature-based applications +/// where subscopes can introduce indirect cycles that span different scopes. +/// +/// The detector tracks resolution chains and throws [CircularDependencyException] +/// when a cycle is found, showing the full chain (including scope context). +/// +/// Example usage via [GlobalCycleDetectionMixin]: +/// ```dart +/// class MyScope with GlobalCycleDetectionMixin { /* ... */ } +/// +/// final scope = MyScope(); +/// scope.setScopeId('feature'); +/// scope.enableGlobalCycleDetection(); +/// +/// scope.withGlobalCycleDetection(String, null, () { +/// // ... resolve dependencies here, will detect cross-scope cycles +/// }); +/// ``` class GlobalCycleDetector { static GlobalCycleDetector? _instance; final CherryPickObserver _observer; - - // Глобальный стек разрешения зависимостей + + // Global set and chain history for all resolutions final Set _globalResolutionStack = HashSet(); - - // История разрешения для построения цепочки зависимостей final List _globalResolutionHistory = []; - - // Карта активных детекторов по скоупам + + // Map of active detectors for subscopes (rarely used directly) final Map _scopeDetectors = HashMap(); GlobalCycleDetector._internal({required CherryPickObserver observer}): _observer = observer; - /// RU: Получить единственный экземпляр глобального детектора. - /// ENG: Get singleton instance of global detector. + /// Returns the singleton global detector instance, initializing it if needed. static GlobalCycleDetector get instance { - _instance ??= GlobalCycleDetector._internal(observer: CherryPick.globalObserver); + _instance ??= GlobalCycleDetector._internal(observer: CherryPick.globalObserver); return _instance!; } - /// RU: Сбросить глобальный детектор (полезно для тестов). - /// ENG: Reset global detector (useful for tests). + /// Reset internal state (useful for testing). static void reset() { _instance?._globalResolutionStack.clear(); _instance?._globalResolutionHistory.clear(); @@ -49,49 +63,38 @@ class GlobalCycleDetector { _instance = null; } - /// RU: Начать отслеживание разрешения зависимости в глобальном контексте. - /// ENG: Start tracking dependency resolution in global context. + /// Start tracking resolution of dependency [T] with optional [named] and [scopeId]. + /// Throws [CircularDependencyException] on cycle. void startGlobalResolving({String? named, String? scopeId}) { final dependencyKey = _createDependencyKeyFromType(T, named, scopeId); - + if (_globalResolutionStack.contains(dependencyKey)) { - // Найдена глобальная циклическая зависимость final cycleStartIndex = _globalResolutionHistory.indexOf(dependencyKey); final cycle = _globalResolutionHistory.sublist(cycleStartIndex)..add(dependencyKey); - _observer.onCycleDetected( - cycle, - scopeName: scopeId, - ); - _observer.onError( - 'Global circular dependency detected for $dependencyKey', - null, - null, - ); + _observer.onCycleDetected(cycle, scopeName: scopeId); + _observer.onError('Global circular dependency detected for $dependencyKey', null, null); throw CircularDependencyException( 'Global circular dependency detected for $dependencyKey', cycle, ); } - + _globalResolutionStack.add(dependencyKey); _globalResolutionHistory.add(dependencyKey); } - /// RU: Завершить отслеживание разрешения зависимости в глобальном контексте. - /// ENG: Finish tracking dependency resolution in global context. + /// Finish tracking a dependency. Should always be called after [startGlobalResolving]. void finishGlobalResolving({String? named, String? scopeId}) { final dependencyKey = _createDependencyKeyFromType(T, named, scopeId); _globalResolutionStack.remove(dependencyKey); - - // Удаляем из истории только если это последний элемент - if (_globalResolutionHistory.isNotEmpty && - _globalResolutionHistory.last == dependencyKey) { + + if (_globalResolutionHistory.isNotEmpty && _globalResolutionHistory.last == dependencyKey) { _globalResolutionHistory.removeLast(); } } - /// RU: Выполнить действие с глобальным отслеживанием циклических зависимостей. - /// ENG: Execute action with global circular dependency tracking. + /// Internally execute [action] with global cycle detection for [dependencyType], [named], [scopeId]. + /// Throws [CircularDependencyException] on cycle. T withGlobalCycleDetection( Type dependencyType, String? named, @@ -99,20 +102,12 @@ class GlobalCycleDetector { T Function() action, ) { final dependencyKey = _createDependencyKeyFromType(dependencyType, named, scopeId); - + if (_globalResolutionStack.contains(dependencyKey)) { final cycleStartIndex = _globalResolutionHistory.indexOf(dependencyKey); - final cycle = _globalResolutionHistory.sublist(cycleStartIndex) - ..add(dependencyKey); - _observer.onCycleDetected( - cycle, - scopeName: scopeId, - ); - _observer.onError( - 'Global circular dependency detected for $dependencyKey', - null, - null, - ); + final cycle = _globalResolutionHistory.sublist(cycleStartIndex)..add(dependencyKey); + _observer.onCycleDetected(cycle, scopeName: scopeId); + _observer.onError('Global circular dependency detected for $dependencyKey', null, null); throw CircularDependencyException( 'Global circular dependency detected for $dependencyKey', cycle, @@ -126,38 +121,32 @@ class GlobalCycleDetector { return action(); } finally { _globalResolutionStack.remove(dependencyKey); - if (_globalResolutionHistory.isNotEmpty && - _globalResolutionHistory.last == dependencyKey) { + if (_globalResolutionHistory.isNotEmpty && _globalResolutionHistory.last == dependencyKey) { _globalResolutionHistory.removeLast(); } } } - /// RU: Получить детектор для конкретного скоупа. - /// ENG: Get detector for specific scope. + /// Get per-scope detector (not usually needed by consumers). CycleDetector getScopeDetector(String scopeId) { return _scopeDetectors.putIfAbsent(scopeId, () => CycleDetector(observer: CherryPick.globalObserver)); } - /// RU: Удалить детектор для скоупа. - /// ENG: Remove detector for scope. + /// Remove detector for a given scope. void removeScopeDetector(String scopeId) { _scopeDetectors.remove(scopeId); } - /// RU: Проверить, находится ли зависимость в процессе глобального разрешения. - /// ENG: Check if dependency is currently being resolved globally. + /// Returns true if dependency [T] is currently being resolved in the global scope. bool isGloballyResolving({String? named, String? scopeId}) { final dependencyKey = _createDependencyKeyFromType(T, named, scopeId); return _globalResolutionStack.contains(dependencyKey); } - /// RU: Получить текущую глобальную цепочку разрешения зависимостей. - /// ENG: Get current global dependency resolution chain. + /// Get current global dependency resolution chain (for debugging or diagnostics). List get globalResolutionChain => List.unmodifiable(_globalResolutionHistory); - /// RU: Очистить все состояние детектора. - /// ENG: Clear all detector state. + /// Clears all global and per-scope state in this detector. void clear() { _globalResolutionStack.clear(); _globalResolutionHistory.clear(); @@ -167,14 +156,7 @@ class GlobalCycleDetector { void _detectorClear(detector) => detector.clear(); - /// RU: Создать уникальный ключ для зависимости с учетом скоупа. - /// ENG: Create unique key for dependency including scope. - //String _createDependencyKey(String? named, String? scopeId) { - // return _createDependencyKeyFromType(T, named, scopeId); - //} - - /// RU: Создать уникальный ключ для зависимости по типу с учетом скоупа. - /// ENG: Create unique key for dependency by type including scope. + /// Creates a unique dependency key string including scope and name (for diagnostics/cycle checks). String _createDependencyKeyFromType(Type type, String? named, String? scopeId) { final typeName = type.toString(); final namePrefix = named != null ? '@$named' : ''; @@ -183,40 +165,53 @@ class GlobalCycleDetector { } } -/// RU: Улучшенный миксин для глобального обнаружения циклических зависимостей. -/// ENG: Enhanced mixin for global circular dependency detection. +/// Enhanced mixin for global circular dependency detection, to be mixed into +/// DI scopes or containers that want cross-scope protection. +/// +/// Typical usage pattern: +/// ```dart +/// class MySubscope with GlobalCycleDetectionMixin { ... } +/// +/// final scope = MySubscope(); +/// scope.setScopeId('user_profile'); +/// scope.enableGlobalCycleDetection(); +/// +/// scope.withGlobalCycleDetection(UserService, null, () { +/// // ... resolve user service and friends, auto-detects global cycles +/// }); +/// ``` mixin GlobalCycleDetectionMixin { String? _scopeId; bool _globalCycleDetectionEnabled = false; - /// RU: Установить идентификатор скоупа для глобального отслеживания. - /// ENG: Set scope identifier for global tracking. + /// Set the scope's unique identifier for global tracking (should be called at scope initialization). void setScopeId(String scopeId) { _scopeId = scopeId; } - /// RU: Получить идентификатор скоупа. - /// ENG: Get scope identifier. + /// Get the scope's id, if configured. String? get scopeId => _scopeId; - /// RU: Включить глобальное обнаружение циклических зависимостей. - /// ENG: Enable global circular dependency detection. + /// Enable global cross-scope circular dependency detection. void enableGlobalCycleDetection() { _globalCycleDetectionEnabled = true; } - /// RU: Отключить глобальное обнаружение циклических зависимостей. - /// ENG: Disable global circular dependency detection. + /// Disable global cycle detection (no cycle checks will be performed globally). void disableGlobalCycleDetection() { _globalCycleDetectionEnabled = false; } - /// RU: Проверить, включено ли глобальное обнаружение циклических зависимостей. - /// ENG: Check if global circular dependency detection is enabled. + /// Returns true if global cycle detection is currently enabled for this scope/container. bool get isGlobalCycleDetectionEnabled => _globalCycleDetectionEnabled; - /// RU: Выполнить действие с глобальным отслеживанием циклических зависимостей. - /// ENG: Execute action with global circular dependency tracking. + /// Executes [action] with global cycle detection for [dependencyType] and [named]. + /// Throws [CircularDependencyException] if a cycle is detected. + /// + /// Example: + /// ```dart + /// withGlobalCycleDetection(UserService, null, () => resolveUser()); + /// ``` T withGlobalCycleDetection( Type dependencyType, String? named, @@ -234,8 +229,7 @@ mixin GlobalCycleDetectionMixin { ); } - /// RU: Получить текущую глобальную цепочку разрешения зависимостей. - /// ENG: Get current global dependency resolution chain. - List get globalResolutionChain => + /// Access the current global dependency resolution chain for diagnostics. + List get globalResolutionChain => GlobalCycleDetector.instance.globalResolutionChain; } diff --git a/cherrypick/lib/src/module.dart b/cherrypick/lib/src/module.dart index 95022eb..8559583 100644 --- a/cherrypick/lib/src/module.dart +++ b/cherrypick/lib/src/module.dart @@ -15,39 +15,71 @@ import 'dart:collection'; import 'package:cherrypick/src/binding.dart'; import 'package:cherrypick/src/scope.dart'; -/// RU: Класс Module является основой для пользовательских модулей. -/// Этот класс нужен для инициализации [Scope]. -/// -/// RU: The Module class is the basis for custom modules. -/// This class is needed to initialize [Scope]. +/// Represents a DI module—a reusable group of dependency bindings. +/// +/// Extend [Module] to declaratively group related [Binding] definitions, +/// then install your module(s) into a [Scope] for dependency resolution. +/// +/// Modules make it easier to organize your DI configuration for features, layers, +/// infrastructure, or integration, and support modular app architecture. +/// +/// Usage example: +/// ```dart +/// class AppModule extends Module { +/// @override +/// void builder(Scope currentScope) { +/// bind().toProvide(() => NetworkService()); +/// bind().toProvide(() => AuthService(currentScope.resolve())); +/// bind().toInstance(Config.dev()); +/// } +/// } +/// +/// // Installing the module into the root DI scope: +/// final rootScope = CherryPick.openRootScope(); +/// rootScope.installModules([AppModule()]); +/// ``` +/// +/// Combine several modules and submodules to implement scalable architectures. /// abstract class Module { final Set _bindingSet = HashSet(); - /// RU: Метод добавляет в коллекцию модуля [Binding] экземпляр. + /// Begins the declaration of a new binding within this module. /// - /// ENG: The method adds an instance to the collection of the [Binding] module. + /// Typically used within [builder] to register all needed dependency bindings. /// - /// return [Binding] + /// Example: + /// ```dart + /// bind().toProvide(() => MockApi()); + /// bind().toInstance(Config.dev()); + /// ``` Binding bind() { final binding = Binding(); _bindingSet.add(binding); return binding; } - /// RU: Метод возвращает коллекцию [Binding] экземпляров. + /// Returns the set of all [Binding] instances registered in this module. /// - /// ENG: The method returns a collection of [Binding] instances. - /// - /// return [Set] + /// This is typically used internally by [Scope] during module installation, + /// but can also be used for diagnostics or introspection. Set get bindingSet => _bindingSet; - /// RU: Абстрактный метод для инициализации пользовательских экземпляров. - /// В этом методе осуществляется конфигурация зависимостей. + /// Abstract method where all dependency bindings are registered. /// - /// ENG: Abstract method for initializing custom instances. - /// This method configures dependencies. + /// Override this method in your custom module subclass to declare + /// all dependency bindings to be provided by this module. /// - /// return [void] + /// The provided [currentScope] can be used for resolving other dependencies, + /// accessing configuration, or controlling binding behavior dynamically. + /// + /// Example (with dependency chaining): + /// ```dart + /// @override + /// void builder(Scope currentScope) { + /// bind().toProvide(() => RestApi()); + /// bind().toProvide(() => UserRepo(currentScope.resolve())); + /// } + /// ``` void builder(Scope currentScope); } diff --git a/cherrypick/lib/src/scope.dart b/cherrypick/lib/src/scope.dart index 21a11ef..5fb9145 100644 --- a/cherrypick/lib/src/scope.dart +++ b/cherrypick/lib/src/scope.dart @@ -21,6 +21,37 @@ import 'package:cherrypick/src/module.dart'; import 'package:cherrypick/src/observer.dart'; // import 'package:cherrypick/src/log_format.dart'; +/// Represents a DI scope (container) for modules, subscopes, +/// and dependency resolution (sync/async) in CherryPick. +/// +/// Scopes provide hierarchical DI: you can resolve dependencies from parents, +/// override or isolate modules, and manage scope-specific singletons. +/// +/// Each scope: +/// - Can install modules ([installModules]) that define [Binding]s +/// - Supports parent-child scope tree (see [openSubScope]) +/// - Can resolve dependencies synchronously ([resolve]) or asynchronously ([resolveAsync]) +/// - Cleans up resources for [Disposable] objects (see [dispose]) +/// - Detects dependency cycles (local and global, if enabled) +/// +/// Example usage: +/// ```dart +/// final rootScope = CherryPick.openRootScope(); +/// rootScope.installModules([AppModule()]); +/// +/// // Synchronous resolution: +/// final auth = rootScope.resolve(); +/// +/// // Asynchronous resolution: +/// final db = await rootScope.resolveAsync(); +/// +/// // Open a child scope (for a feature, page, or test): +/// final userScope = rootScope.openSubScope('user'); +/// userScope.installModules([UserModule()]); +/// +/// // Proper resource cleanup (calls dispose() on tracked objects) +/// await CherryPick.closeRootScope(); +/// ``` class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin { final Scope? _parentScope; @@ -32,11 +63,7 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin { /// COLLECTS all resolved instances that implement [Disposable]. final Set _disposables = HashSet(); - /// RU: Метод возвращает родительский [Scope]. - /// - /// ENG: The method returns the parent [Scope]. - /// - /// return [Scope] + /// Returns the parent [Scope] if present, or null if this is the root scope. Scope? get parentScope => _parentScope; final Map _scopeMap = HashMap(); @@ -61,8 +88,9 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin { final Map> _bindingResolvers = {}; - /// RU: Генерирует уникальный идентификатор для скоупа. - /// ENG: Generates unique identifier for scope. + /// Generates a unique identifier string for this scope instance. + /// + /// Used internally for diagnostics, logging and global scope tracking. String _generateScopeId() { final random = Random(); final timestamp = DateTime.now().millisecondsSinceEpoch; @@ -70,16 +98,20 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin { return 'scope_${timestamp}_$randomPart'; } - /// RU: Метод открывает дочерний (дополнительный) [Scope]. + /// Opens a named child [Scope] (subscope) as a descendant of the current scope. /// - /// ENG: The method opens child (additional) [Scope]. + /// Subscopes inherit modules and DI context from their parent, but can override or extend bindings. + /// Useful for feature-isolation, screens, request/transaction lifetimes, and test separation. /// - /// return [Scope] + /// Example: + /// ```dart + /// final featureScope = rootScope.openSubScope('feature'); + /// featureScope.installModules([FeatureModule()]); + /// final dep = featureScope.resolve(); + /// ``` Scope openSubScope(String name) { if (!_scopeMap.containsKey(name)) { - final childScope = Scope(this, observer: observer); // Наследуем observer вниз по иерархии - // print removed (trace) - // Наследуем настройки обнаружения циклических зависимостей + final childScope = Scope(this, observer: observer); if (isCycleDetectionEnabled) { childScope.enableCycleDetection(); } @@ -101,16 +133,19 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin { return _scopeMap[name]!; } - /// RU: Метод закрывает дочерний (дополнительный) [Scope] асинхронно. + /// Asynchronously closes and disposes a named child [Scope] (subscope). /// - /// ENG: The method closes child (additional) [Scope] asynchronously. + /// Ensures all [Disposable] objects and internal modules + /// in the subscope are properly cleaned up. Also removes any global cycle detectors associated with the subscope. /// - /// return [Future] + /// Example: + /// ```dart + /// await rootScope.closeSubScope('feature'); + /// ``` Future closeSubScope(String name) async { final childScope = _scopeMap[name]; if (childScope != null) { - await childScope.dispose(); // асинхронный вызов - // Очищаем детектор для дочернего скоупа + await childScope.dispose(); if (childScope.scopeId != null) { GlobalCycleDetector.instance.removeScopeDetector(childScope.scopeId!); } @@ -129,11 +164,15 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin { _scopeMap.remove(name); } - /// RU: Метод инициализирует пользовательские модули в [Scope]. + /// Installs a list of custom [Module]s into the [Scope]. /// - /// ENG: The method initializes custom modules in [Scope]. + /// Each module registers bindings and configuration for dependencies. + /// After calling this, bindings are immediately available for resolve/tryResolve. /// - /// return [Scope] + /// Example: + /// ```dart + /// rootScope.installModules([AppModule(), NetworkModule()]); + /// ``` Scope installModules(List modules) { _modulesList.addAll(modules); if (modules.isNotEmpty) { @@ -153,7 +192,7 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin { }, ); module.builder(this); - // После builder: для всех новых биндингов + // Associate bindings with this scope's observer for (final binding in module.bindingSet) { binding.observer = observer; binding.logAllDeferred(); @@ -163,11 +202,15 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin { return this; } - /// RU: Метод удаляет пользовательские модули из [Scope]. + /// Removes all installed [Module]s and their bindings from this [Scope]. /// - /// ENG: This method removes custom modules from [Scope]. + /// Typically used in tests or when resetting app configuration/runtime environment. + /// Note: this does not dispose resolved [Disposable]s (call [dispose] for that). /// - /// return [Scope] + /// Example: + /// ```dart + /// testScope.dropModules(); + /// ``` Scope dropModules() { if (_modulesList.isNotEmpty) { observer.onModulesRemoved( @@ -188,24 +231,22 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin { return this; } - /// RU: Возвращает найденную зависимость, определенную параметром типа [T]. - /// Выдает [StateError], если зависимость не может быть разрешена. - /// Если вы хотите получить [null], если зависимость не может быть найдена, - /// то используйте вместо этого [tryResolve] - /// return - возвращает объект типа [T] или [StateError] + /// Resolves a dependency of type [T], optionally by name and with params. /// - /// ENG: Returns the found dependency specified by the type parameter [T]. - /// Throws [StateError] if the dependency cannot be resolved. - /// If you want to get [null] if the dependency cannot be found then use [tryResolve] instead - /// return - returns an object of type [T] or [StateError] + /// Throws [StateError] if the dependency cannot be resolved. (Use [tryResolve] for fallible lookup). + /// Resolves from installed modules or recurses up the parent scope chain. /// + /// Example: + /// ```dart + /// final logger = scope.resolve(); + /// final special = scope.resolve(named: 'special'); + /// ``` T resolve({String? named, dynamic params}) { observer.onInstanceRequested(T.toString(), T, scopeName: scopeId); - // Используем глобальное отслеживание, если включено T result; if (isGlobalCycleDetectionEnabled) { try { - result = withGlobalCycleDetection(T, named, () { + result = withGlobalCycleDetection(T, named, () { return _resolveWithLocalDetection(named: named, params: params); }); } catch (e, s) { @@ -232,8 +273,9 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin { return result; } - /// RU: Разрешение с локальным детектором циклических зависимостей. - /// ENG: Resolution with local circular dependency detector. + /// Resolves [T] using the local cycle detector for this scope. + /// Throws [StateError] if not found or cycle is detected. + /// Used internally by [resolve]. T _resolveWithLocalDetection({String? named, dynamic params}) { return withCycleDetection(T, named, () { var resolved = _tryResolveInternal(named: named, params: params); @@ -262,11 +304,16 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin { }); } - /// RU: Возвращает найденную зависимость типа [T] или null, если она не может быть найдена. - /// ENG: Returns found dependency of type [T] or null if it cannot be found. + /// Attempts to resolve a dependency of type [T], optionally by name and with params. /// + /// Returns the resolved dependency, or `null` if not found. + /// Does not throw if missing (unlike [resolve]). + /// + /// Example: + /// ```dart + /// final maybeDb = scope.tryResolve(); + /// ``` T? tryResolve({String? named, dynamic params}) { - // Используем глобальное отслеживание, если включено T? result; if (isGlobalCycleDetectionEnabled) { result = withGlobalCycleDetection(T, named, () { @@ -279,8 +326,8 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin { return result; } - /// RU: Попытка разрешения с локальным детектором циклических зависимостей. - /// ENG: Try resolution with local circular dependency detector. + /// Attempts to resolve [T] using the local cycle detector. Returns null if not found or cycle. + /// Used internally by [tryResolve]. T? _tryResolveWithLocalDetection({String? named, dynamic params}) { if (isCycleDetectionEnabled) { return withCycleDetection(T, named, () { @@ -291,29 +338,25 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin { } } - /// RU: Внутренний метод для разрешения зависимостей без проверки циклических зависимостей. - /// ENG: Internal method for dependency resolution without circular dependency checking. + /// Locates and resolves [T] without cycle detection (direct lookup). + /// Returns null if not found. Used internally. T? _tryResolveInternal({String? named, dynamic params}) { final resolver = _findBindingResolver(named); - - // 1 Поиск зависимости по всем модулям текущего скоупа + // 1 - Try from own modules; 2 - Fallback to parent return resolver?.resolveSync(params) ?? - // 2 Поиск зависимостей в родительском скоупе _parentScope?.tryResolve(named: named, params: params); } - /// RU: Асинхронно возвращает найденную зависимость, определенную параметром типа [T]. - /// Выдает [StateError], если зависимость не может быть разрешена. - /// Если хотите получить [null], если зависимость не может быть найдена, используйте [tryResolveAsync]. - /// return - возвращает объект типа [T] or [StateError] + /// Asynchronously resolves a dependency of type [T]. /// - /// ENG: Asynchronously returns the found dependency specified by the type parameter [T]. - /// Throws [StateError] if the dependency cannot be resolved. - /// If you want to get [null] if the dependency cannot be found, use [tryResolveAsync] instead. - /// return - returns an object of type [T] or [StateError] + /// Throws [StateError] if not found. (Use [tryResolveAsync] for a fallible async resolve.) /// + /// Example: + /// ```dart + /// final db = await scope.resolveAsync(); + /// final special = await scope.resolveAsync(named: "special"); + /// ``` Future resolveAsync({String? named, dynamic params}) async { - // Используем глобальное отслеживание, если включено T result; if (isGlobalCycleDetectionEnabled) { result = await withGlobalCycleDetection>(T, named, () async { @@ -326,8 +369,8 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin { return result; } - /// RU: Асинхронное разрешение с локальным детектором циклических зависимостей. - /// ENG: Async resolution with local circular dependency detector. + /// Resolves [T] asynchronously using local cycle detector. Throws if not found. + /// Internal implementation for async [resolveAsync]. Future _resolveAsyncWithLocalDetection({String? named, dynamic params}) async { return withCycleDetection>(T, named, () async { var resolved = await _tryResolveAsyncInternal(named: named, params: params); @@ -356,8 +399,14 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin { }); } + /// Attempts to asynchronously resolve a dependency of type [T]. + /// Returns the dependency or null if not present (never throws). + /// + /// Example: + /// ```dart + /// final user = await scope.tryResolveAsync(); + /// ``` Future tryResolveAsync({String? named, dynamic params}) async { - // Используем глобальное отслеживание, если включено T? result; if (isGlobalCycleDetectionEnabled) { result = await withGlobalCycleDetection>(T, named, () async { @@ -370,8 +419,8 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin { return result; } - /// RU: Асинхронная попытка разрешения с локальным детектором циклических зависимостей. - /// ENG: Async try resolution with local circular dependency detector. + /// Attempts to resolve [T] asynchronously using local cycle detector. Returns null if missing. + /// Internal implementation for async [tryResolveAsync]. Future _tryResolveAsyncWithLocalDetection({String? named, dynamic params}) async { if (isCycleDetectionEnabled) { return withCycleDetection>(T, named, () async { @@ -382,21 +431,21 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin { } } - /// RU: Внутренний метод для асинхронного разрешения зависимостей без проверки циклических зависимостей. - /// ENG: Internal method for async dependency resolution without circular dependency checking. + /// Direct async resolution for [T] without cycle check. Returns null if missing. Internal use only. Future _tryResolveAsyncInternal({String? named, dynamic params}) async { final resolver = _findBindingResolver(named); - - // 1 Поиск зависимости по всем модулям текущего скоупа + // 1 - Try from own modules; 2 - Fallback to parent return resolver?.resolveAsync(params) ?? - // 2 Поиск зависимостей в родительском скоупе _parentScope?.tryResolveAsync(named: named, params: params); } + /// Looks up the [BindingResolver] for [T] and [named] within this scope. + /// Returns null if none found. Internal use only. BindingResolver? _findBindingResolver(String? named) => _bindingResolvers[T]?[named] as BindingResolver?; - // Индексируем все binding’и после каждого installModules/dropModules + /// Rebuilds the internal index of all [BindingResolver]s from installed modules. + /// Called after [installModules] and [dropModules]. Internal use only. void _rebuildResolversIndex() { _bindingResolvers.clear(); for (var module in _modulesList) { @@ -408,14 +457,24 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin { } } - /// INTERNAL: Tracks Disposable objects + /// Tracks resolved [Disposable] instances, to ensure dispose is called automatically. + /// Internal use only. void _trackDisposable(Object? obj) { if (obj is Disposable && !_disposables.contains(obj)) { _disposables.add(obj); } } - /// Calls dispose on all tracked disposables and child scopes recursively (async). + /// Asynchronously disposes this [Scope], all tracked [Disposable] objects, and recursively + /// all its child subscopes. + /// + /// This method should always be called when a scope is no longer needed + /// to guarantee timely resource cleanup (files, sockets, streams, handles, etc). + /// + /// Example: + /// ```dart + /// await myScope.dispose(); + /// ``` Future dispose() async { // First dispose children scopes for (final subScope in _scopeMap.values) { diff --git a/examples/client_app/pubspec.lock b/examples/client_app/pubspec.lock index de5aa91..46cc56a 100644 --- a/examples/client_app/pubspec.lock +++ b/examples/client_app/pubspec.lock @@ -127,7 +127,7 @@ packages: path: "../../cherrypick" relative: true source: path - version: "3.0.0-dev.7" + version: "3.0.0-dev.8" cherrypick_annotations: dependency: "direct main" description: @@ -141,7 +141,7 @@ packages: path: "../../cherrypick_flutter" relative: true source: path - version: "1.1.3-dev.7" + version: "1.1.3-dev.8" cherrypick_generator: dependency: "direct dev" description: diff --git a/examples/postly/pubspec.lock b/examples/postly/pubspec.lock index e6c0806..eda5d36 100644 --- a/examples/postly/pubspec.lock +++ b/examples/postly/pubspec.lock @@ -175,7 +175,7 @@ packages: path: "../../cherrypick" relative: true source: path - version: "3.0.0-dev.7" + version: "3.0.0-dev.8" cherrypick_annotations: dependency: "direct main" description: diff --git a/pubspec.lock b/pubspec.lock index 89c1b0a..eb70210 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,23 +5,23 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: "16e298750b6d0af7ce8a3ba7c18c69c3785d11b15ec83f6dcd0ad2a0009b3cab" + sha256: "45cfa8471b89fb6643fe9bf51bd7931a76b8f5ec2d65de4fb176dba8d4f22c77" url: "https://pub.dev" source: hosted - version: "76.0.0" + version: "73.0.0" _macros: dependency: transitive description: dart source: sdk - version: "0.3.3" + version: "0.3.2" analyzer: dependency: transitive description: name: analyzer - sha256: "1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e" + sha256: "4959fec185fe70cce007c57e9ab6983101dbe593d2bf8bbfb4453aaec0cf470a" url: "https://pub.dev" source: hosted - version: "6.11.0" + version: "6.8.0" ansi_styles: dependency: transitive description: @@ -298,10 +298,10 @@ packages: dependency: transitive description: name: macros - sha256: "1d9e801cd66f7ea3663c45fc708450db1fa57f988142c64289142c9b7ee80656" + sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536" url: "https://pub.dev" source: hosted - version: "0.1.3-main.0" + version: "0.1.2-main.4" matcher: dependency: transitive description: