mirror of
https://github.com/pese-git/cherrypick.git
synced 2026-05-16 10:10:43 +00:00
feat(cherrypick): optimize Scope resolution with incremental index, silent observer, and fast-path
This commit is contained in:
@@ -19,7 +19,7 @@ import 'package:cherrypick/src/global_cycle_detector.dart';
|
|||||||
import 'package:cherrypick/src/binding_resolver.dart';
|
import 'package:cherrypick/src/binding_resolver.dart';
|
||||||
import 'package:cherrypick/src/module.dart';
|
import 'package:cherrypick/src/module.dart';
|
||||||
import 'package:cherrypick/src/observer.dart';
|
import 'package:cherrypick/src/observer.dart';
|
||||||
// import 'package:cherrypick/src/log_format.dart';
|
|
||||||
|
|
||||||
/// Represents a DI scope (container) for modules, subscopes,
|
/// Represents a DI scope (container) for modules, subscopes,
|
||||||
/// and dependency resolution (sync/async) in CherryPick.
|
/// and dependency resolution (sync/async) in CherryPick.
|
||||||
@@ -60,6 +60,13 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
|||||||
@override
|
@override
|
||||||
CherryPickObserver get observer => _observer;
|
CherryPickObserver get observer => _observer;
|
||||||
|
|
||||||
|
bool get _isSilentObserver => _observer is SilentCherryPickObserver;
|
||||||
|
|
||||||
|
bool get _canUseDirectResolvePath =>
|
||||||
|
_isSilentObserver &&
|
||||||
|
!isCycleDetectionEnabled &&
|
||||||
|
!isGlobalCycleDetectionEnabled;
|
||||||
|
|
||||||
/// COLLECTS all resolved instances that implement [Disposable].
|
/// COLLECTS all resolved instances that implement [Disposable].
|
||||||
final Set<Disposable> _disposables = HashSet();
|
final Set<Disposable> _disposables = HashSet();
|
||||||
|
|
||||||
@@ -71,21 +78,23 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
|||||||
Scope(this._parentScope, {required CherryPickObserver observer})
|
Scope(this._parentScope, {required CherryPickObserver observer})
|
||||||
: _observer = observer {
|
: _observer = observer {
|
||||||
setScopeId(_generateScopeId());
|
setScopeId(_generateScopeId());
|
||||||
observer.onScopeOpened(scopeId ?? 'NO_ID');
|
if (!_isSilentObserver) {
|
||||||
observer.onDiagnostic(
|
observer.onScopeOpened(scopeId ?? 'NO_ID');
|
||||||
'Scope created: ${scopeId ?? 'NO_ID'}',
|
observer.onDiagnostic(
|
||||||
details: {
|
'Scope created: ${scopeId ?? 'NO_ID'}',
|
||||||
'type': 'Scope',
|
details: {
|
||||||
'name': scopeId ?? 'NO_ID',
|
'type': 'Scope',
|
||||||
if (_parentScope?.scopeId != null) 'parent': _parentScope!.scopeId,
|
'name': scopeId ?? 'NO_ID',
|
||||||
'description': 'scope created',
|
if (_parentScope?.scopeId != null) 'parent': _parentScope!.scopeId,
|
||||||
},
|
'description': 'scope created',
|
||||||
);
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final Set<Module> _modulesList = HashSet();
|
final Set<Module> _modulesList = HashSet();
|
||||||
|
|
||||||
// индекс для мгновенного поиска binding’ов
|
// index for fast binding lookup
|
||||||
final Map<Object, Map<String?, BindingResolver>> _bindingResolvers = {};
|
final Map<Object, Map<String?, BindingResolver>> _bindingResolvers = {};
|
||||||
|
|
||||||
/// Generates a unique identifier string for this scope instance.
|
/// Generates a unique identifier string for this scope instance.
|
||||||
@@ -119,16 +128,18 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
|||||||
childScope.enableGlobalCycleDetection();
|
childScope.enableGlobalCycleDetection();
|
||||||
}
|
}
|
||||||
_scopeMap[name] = childScope;
|
_scopeMap[name] = childScope;
|
||||||
observer.onDiagnostic(
|
if (!_isSilentObserver) {
|
||||||
'SubScope created: $name',
|
observer.onDiagnostic(
|
||||||
details: {
|
'SubScope created: $name',
|
||||||
'type': 'SubScope',
|
details: {
|
||||||
'name': name,
|
'type': 'SubScope',
|
||||||
'id': childScope.scopeId,
|
'name': name,
|
||||||
if (scopeId != null) 'parent': scopeId,
|
'id': childScope.scopeId,
|
||||||
'description': 'subscope created',
|
if (scopeId != null) 'parent': scopeId,
|
||||||
},
|
'description': 'subscope created',
|
||||||
);
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return _scopeMap[name]!;
|
return _scopeMap[name]!;
|
||||||
}
|
}
|
||||||
@@ -149,17 +160,19 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
|||||||
if (childScope.scopeId != null) {
|
if (childScope.scopeId != null) {
|
||||||
GlobalCycleDetector.instance.removeScopeDetector(childScope.scopeId!);
|
GlobalCycleDetector.instance.removeScopeDetector(childScope.scopeId!);
|
||||||
}
|
}
|
||||||
observer.onScopeClosed(childScope.scopeId ?? name);
|
if (!_isSilentObserver) {
|
||||||
observer.onDiagnostic(
|
observer.onScopeClosed(childScope.scopeId ?? name);
|
||||||
'SubScope closed: $name',
|
observer.onDiagnostic(
|
||||||
details: {
|
'SubScope closed: $name',
|
||||||
'type': 'SubScope',
|
details: {
|
||||||
'name': name,
|
'type': 'SubScope',
|
||||||
'id': childScope.scopeId,
|
'name': name,
|
||||||
if (scopeId != null) 'parent': scopeId,
|
'id': childScope.scopeId,
|
||||||
'description': 'subscope closed',
|
if (scopeId != null) 'parent': scopeId,
|
||||||
},
|
'description': 'subscope closed',
|
||||||
);
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_scopeMap.remove(name);
|
_scopeMap.remove(name);
|
||||||
}
|
}
|
||||||
@@ -175,30 +188,34 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
|||||||
/// ```
|
/// ```
|
||||||
Scope installModules(List<Module> modules) {
|
Scope installModules(List<Module> modules) {
|
||||||
_modulesList.addAll(modules);
|
_modulesList.addAll(modules);
|
||||||
if (modules.isNotEmpty) {
|
if (!_isSilentObserver && modules.isNotEmpty) {
|
||||||
observer.onModulesInstalled(
|
observer.onModulesInstalled(
|
||||||
modules.map((m) => m.runtimeType.toString()).toList(),
|
modules.map((m) => m.runtimeType.toString()).toList(),
|
||||||
scopeName: scopeId,
|
scopeName: scopeId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
for (var module in modules) {
|
for (var module in modules) {
|
||||||
observer.onDiagnostic(
|
if (!_isSilentObserver) {
|
||||||
'Module installed: ${module.runtimeType}',
|
observer.onDiagnostic(
|
||||||
details: {
|
'Module installed: ${module.runtimeType}',
|
||||||
'type': 'Module',
|
details: {
|
||||||
'name': module.runtimeType.toString(),
|
'type': 'Module',
|
||||||
'scope': scopeId,
|
'name': module.runtimeType.toString(),
|
||||||
'description': 'module installed',
|
'scope': scopeId,
|
||||||
},
|
'description': 'module installed',
|
||||||
);
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
module.builder(this);
|
module.builder(this);
|
||||||
// Associate bindings with this scope's observer
|
// Associate bindings with this scope's observer
|
||||||
for (final binding in module.bindingSet) {
|
for (final binding in module.bindingSet) {
|
||||||
binding.observer = observer;
|
binding.observer = observer;
|
||||||
binding.logAllDeferred();
|
if (!_isSilentObserver) {
|
||||||
|
binding.logAllDeferred();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
_addModuleToIndex(module);
|
||||||
}
|
}
|
||||||
_rebuildResolversIndex();
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,20 +229,22 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
|||||||
/// testScope.dropModules();
|
/// testScope.dropModules();
|
||||||
/// ```
|
/// ```
|
||||||
Scope dropModules() {
|
Scope dropModules() {
|
||||||
if (_modulesList.isNotEmpty) {
|
if (!_isSilentObserver && _modulesList.isNotEmpty) {
|
||||||
observer.onModulesRemoved(
|
observer.onModulesRemoved(
|
||||||
_modulesList.map((m) => m.runtimeType.toString()).toList(),
|
_modulesList.map((m) => m.runtimeType.toString()).toList(),
|
||||||
scopeName: scopeId,
|
scopeName: scopeId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
observer.onDiagnostic(
|
if (!_isSilentObserver) {
|
||||||
'Modules dropped for scope: $scopeId',
|
observer.onDiagnostic(
|
||||||
details: {
|
'Modules dropped for scope: $scopeId',
|
||||||
'type': 'Scope',
|
details: {
|
||||||
'name': scopeId,
|
'type': 'Scope',
|
||||||
'description': 'modules dropped',
|
'name': scopeId,
|
||||||
},
|
'description': 'modules dropped',
|
||||||
);
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
_modulesList.clear();
|
_modulesList.clear();
|
||||||
_rebuildResolversIndex();
|
_rebuildResolversIndex();
|
||||||
return this;
|
return this;
|
||||||
@@ -242,6 +261,16 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
|||||||
/// final special = scope.resolve<Service>(named: 'special');
|
/// final special = scope.resolve<Service>(named: 'special');
|
||||||
/// ```
|
/// ```
|
||||||
T resolve<T>({String? named, dynamic params}) {
|
T resolve<T>({String? named, dynamic params}) {
|
||||||
|
if (_canUseDirectResolvePath) {
|
||||||
|
final result = _tryResolveInternal<T>(named: named, params: params);
|
||||||
|
if (result == null) {
|
||||||
|
throw StateError(
|
||||||
|
'Can\'t resolve dependency `$T`. Maybe you forget register it?');
|
||||||
|
}
|
||||||
|
if (result is Disposable) _disposables.add(result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
observer.onInstanceRequested(T.toString(), T, scopeName: scopeId);
|
observer.onInstanceRequested(T.toString(), T, scopeName: scopeId);
|
||||||
T result;
|
T result;
|
||||||
if (isGlobalCycleDetectionEnabled) {
|
if (isGlobalCycleDetectionEnabled) {
|
||||||
@@ -315,6 +344,12 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
|||||||
/// final maybeDb = scope.tryResolve<Database>();
|
/// final maybeDb = scope.tryResolve<Database>();
|
||||||
/// ```
|
/// ```
|
||||||
T? tryResolve<T>({String? named, dynamic params}) {
|
T? tryResolve<T>({String? named, dynamic params}) {
|
||||||
|
if (_canUseDirectResolvePath) {
|
||||||
|
final result = _tryResolveInternal<T>(named: named, params: params);
|
||||||
|
if (result != null && result is Disposable) _disposables.add(result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
T? result;
|
T? result;
|
||||||
if (isGlobalCycleDetectionEnabled) {
|
if (isGlobalCycleDetectionEnabled) {
|
||||||
result = withGlobalCycleDetection<T?>(T, named, () {
|
result = withGlobalCycleDetection<T?>(T, named, () {
|
||||||
@@ -358,6 +393,21 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
|||||||
/// final special = await scope.resolveAsync<Service>(named: "special");
|
/// final special = await scope.resolveAsync<Service>(named: "special");
|
||||||
/// ```
|
/// ```
|
||||||
Future<T> resolveAsync<T>({String? named, dynamic params}) async {
|
Future<T> resolveAsync<T>({String? named, dynamic params}) async {
|
||||||
|
if (_canUseDirectResolvePath) {
|
||||||
|
final result =
|
||||||
|
await _tryResolveAsyncInternal<T>(named: named, params: params);
|
||||||
|
if (result == null) {
|
||||||
|
throw StateError(
|
||||||
|
"Can't resolve async dependency `$T`. Maybe you forget register it?");
|
||||||
|
}
|
||||||
|
if (result is Disposable) _disposables.add(result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
return _resolveAsyncWithObserverPath<T>(named: named, params: params);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<T> _resolveAsyncWithObserverPath<T>(
|
||||||
|
{String? named, dynamic params}) async {
|
||||||
T result;
|
T result;
|
||||||
if (isGlobalCycleDetectionEnabled) {
|
if (isGlobalCycleDetectionEnabled) {
|
||||||
result = await withGlobalCycleDetection<Future<T>>(T, named, () async {
|
result = await withGlobalCycleDetection<Future<T>>(T, named, () async {
|
||||||
@@ -413,6 +463,17 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
|||||||
/// final user = await scope.tryResolveAsync<User>();
|
/// final user = await scope.tryResolveAsync<User>();
|
||||||
/// ```
|
/// ```
|
||||||
Future<T?> tryResolveAsync<T>({String? named, dynamic params}) async {
|
Future<T?> tryResolveAsync<T>({String? named, dynamic params}) async {
|
||||||
|
if (_canUseDirectResolvePath) {
|
||||||
|
final result =
|
||||||
|
await _tryResolveAsyncInternal<T>(named: named, params: params);
|
||||||
|
if (result != null && result is Disposable) _disposables.add(result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
return _tryResolveAsyncWithObserverPath<T>(named: named, params: params);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<T?> _tryResolveAsyncWithObserverPath<T>(
|
||||||
|
{String? named, dynamic params}) async {
|
||||||
T? result;
|
T? result;
|
||||||
if (isGlobalCycleDetectionEnabled) {
|
if (isGlobalCycleDetectionEnabled) {
|
||||||
result = await withGlobalCycleDetection<Future<T?>>(T, named, () async {
|
result = await withGlobalCycleDetection<Future<T?>>(T, named, () async {
|
||||||
@@ -441,12 +502,12 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Direct async resolution for [T] without cycle check. Returns null if missing. Internal use only.
|
/// Direct async resolution for [T] without cycle check. Returns null if missing. Internal use only.
|
||||||
Future<T?> _tryResolveAsyncInternal<T>(
|
Future<T?> _tryResolveAsyncInternal<T>({String? named, dynamic params}) {
|
||||||
{String? named, dynamic params}) async {
|
|
||||||
final resolver = _findBindingResolver<T>(named);
|
final resolver = _findBindingResolver<T>(named);
|
||||||
// 1 - Try from own modules; 2 - Fallback to parent
|
// 1 - Try from own modules; 2 - Fallback to parent
|
||||||
return resolver?.resolveAsync(params) ??
|
return resolver?.resolveAsync(params) ??
|
||||||
_parentScope?.tryResolveAsync(named: named, params: params);
|
_parentScope?.tryResolveAsync(named: named, params: params) ??
|
||||||
|
Future<T?>.value(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Looks up the [BindingResolver] for [T] and [named] within this scope.
|
/// Looks up the [BindingResolver] for [T] and [named] within this scope.
|
||||||
@@ -454,23 +515,27 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
|||||||
BindingResolver<T>? _findBindingResolver<T>(String? named) =>
|
BindingResolver<T>? _findBindingResolver<T>(String? named) =>
|
||||||
_bindingResolvers[T]?[named] as BindingResolver<T>?;
|
_bindingResolvers[T]?[named] as BindingResolver<T>?;
|
||||||
|
|
||||||
|
void _addModuleToIndex(Module module) {
|
||||||
|
for (var binding in module.bindingSet) {
|
||||||
|
_bindingResolvers.putIfAbsent(binding.key, () => {});
|
||||||
|
final nameKey = binding.isNamed ? binding.name : null;
|
||||||
|
_bindingResolvers[binding.key]![nameKey] = binding.resolver!;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Rebuilds the internal index of all [BindingResolver]s from installed modules.
|
/// Rebuilds the internal index of all [BindingResolver]s from installed modules.
|
||||||
/// Called after [installModules] and [dropModules]. Internal use only.
|
/// Called after [dropModules]. Internal use only.
|
||||||
void _rebuildResolversIndex() {
|
void _rebuildResolversIndex() {
|
||||||
_bindingResolvers.clear();
|
_bindingResolvers.clear();
|
||||||
for (var module in _modulesList) {
|
for (var module in _modulesList) {
|
||||||
for (var binding in module.bindingSet) {
|
_addModuleToIndex(module);
|
||||||
_bindingResolvers.putIfAbsent(binding.key, () => {});
|
|
||||||
final nameKey = binding.isNamed ? binding.name : null;
|
|
||||||
_bindingResolvers[binding.key]![nameKey] = binding.resolver!;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Tracks resolved [Disposable] instances, to ensure dispose is called automatically.
|
/// Tracks resolved [Disposable] instances, to ensure dispose is called automatically.
|
||||||
/// Internal use only.
|
/// Internal use only.
|
||||||
void _trackDisposable(Object? obj) {
|
void _trackDisposable(Object? obj) {
|
||||||
if (obj is Disposable && !_disposables.contains(obj)) {
|
if (obj is Disposable) {
|
||||||
_disposables.add(obj);
|
_disposables.add(obj);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -487,14 +552,14 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
|||||||
/// ```
|
/// ```
|
||||||
Future<void> dispose() async {
|
Future<void> dispose() async {
|
||||||
// Create copies to avoid concurrent modification
|
// Create copies to avoid concurrent modification
|
||||||
final scopesCopy = Map<String, Scope>.from(_scopeMap);
|
final scopes = _scopeMap.values.toList();
|
||||||
for (final subScope in scopesCopy.values) {
|
for (final subScope in scopes) {
|
||||||
await subScope.dispose();
|
await subScope.dispose();
|
||||||
}
|
}
|
||||||
_scopeMap.clear();
|
_scopeMap.clear();
|
||||||
|
|
||||||
final disposablesCopy = Set<Disposable>.from(_disposables);
|
final disposables = _disposables.toList();
|
||||||
for (final d in disposablesCopy) {
|
for (final d in disposables) {
|
||||||
await d.dispose();
|
await d.dispose();
|
||||||
}
|
}
|
||||||
_disposables.clear();
|
_disposables.clear();
|
||||||
|
|||||||
148
cherrypick/test/src/scope_fast_path_test.dart
Normal file
148
cherrypick/test/src/scope_fast_path_test.dart
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
import 'package:cherrypick/cherrypick.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
class _IntModule extends Module {
|
||||||
|
final int value;
|
||||||
|
final void Function()? onBuild;
|
||||||
|
|
||||||
|
_IntModule(this.value, {this.onBuild});
|
||||||
|
|
||||||
|
@override
|
||||||
|
void builder(Scope currentScope) {
|
||||||
|
onBuild?.call();
|
||||||
|
bind<int>().toInstance(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _StringModule extends Module {
|
||||||
|
final String value;
|
||||||
|
|
||||||
|
_StringModule(this.value);
|
||||||
|
|
||||||
|
@override
|
||||||
|
void builder(Scope currentScope) {
|
||||||
|
bind<String>().toInstance(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _NamedIntModule extends Module {
|
||||||
|
final String name;
|
||||||
|
final int value;
|
||||||
|
|
||||||
|
_NamedIntModule(this.name, this.value);
|
||||||
|
|
||||||
|
@override
|
||||||
|
void builder(Scope currentScope) {
|
||||||
|
bind<int>().withName(name).toInstance(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DisposableModule extends Module {
|
||||||
|
@override
|
||||||
|
void builder(Scope currentScope) {
|
||||||
|
bind<_DisposableService>().toProvide(() => _DisposableService());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DisposableService implements Disposable {
|
||||||
|
bool disposed = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> dispose() async {
|
||||||
|
disposed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
tearDown(() => CherryPick.closeRootScope());
|
||||||
|
|
||||||
|
group('SilentCherryPickObserver fast-path', () {
|
||||||
|
test('resolve with default silent observer uses fast-path', () {
|
||||||
|
final scope = CherryPick.openSafeRootScope();
|
||||||
|
scope.installModules([_IntModule(42)]);
|
||||||
|
|
||||||
|
final result = scope.resolve<int>();
|
||||||
|
expect(result, 42);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('resolveAsync with default silent observer uses fast-path', () async {
|
||||||
|
final scope = CherryPick.openSafeRootScope();
|
||||||
|
scope.installModules([_IntModule(42)]);
|
||||||
|
|
||||||
|
final result = await scope.resolveAsync<int>();
|
||||||
|
expect(result, 42);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('tryResolve with default silent observer uses fast-path', () {
|
||||||
|
final scope = CherryPick.openSafeRootScope();
|
||||||
|
scope.installModules([_IntModule(42)]);
|
||||||
|
|
||||||
|
final result = scope.tryResolve<int>();
|
||||||
|
expect(result, 42);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('tryResolveAsync with default silent observer uses fast-path',
|
||||||
|
() async {
|
||||||
|
final scope = CherryPick.openSafeRootScope();
|
||||||
|
scope.installModules([_IntModule(42)]);
|
||||||
|
|
||||||
|
final result = await scope.tryResolveAsync<int>();
|
||||||
|
expect(result, 42);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('installModules with silent observer skips diagnostics', () {
|
||||||
|
final scope = CherryPick.openSafeRootScope();
|
||||||
|
var buildCalls = 0;
|
||||||
|
scope.installModules([_IntModule(42, onBuild: () => buildCalls++)]);
|
||||||
|
expect(buildCalls, 1);
|
||||||
|
expect(scope.resolve<int>(), 42);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Disposable is tracked even in silent observer fast-path', () async {
|
||||||
|
final scope = CherryPick.openSafeRootScope();
|
||||||
|
scope.installModules([_DisposableModule()]);
|
||||||
|
|
||||||
|
final service = scope.resolve<_DisposableService>();
|
||||||
|
expect(service.disposed, false);
|
||||||
|
await scope.dispose();
|
||||||
|
expect(service.disposed, true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('Incremental module index', () {
|
||||||
|
test('Multiple installModules calls accumulate bindings', () {
|
||||||
|
final scope = CherryPick.openSafeRootScope();
|
||||||
|
scope.installModules([_IntModule(1)]);
|
||||||
|
scope.installModules([_StringModule('two')]);
|
||||||
|
|
||||||
|
expect(scope.resolve<int>(), 1);
|
||||||
|
expect(scope.resolve<String>(), 'two');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('dropModules clears all bindings', () {
|
||||||
|
final scope = CherryPick.openSafeRootScope();
|
||||||
|
scope.installModules([_IntModule(1)]);
|
||||||
|
scope.dropModules();
|
||||||
|
|
||||||
|
expect(() => scope.resolve<int>(), throwsStateError);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Re-installing modules after dropModules works', () {
|
||||||
|
final scope = CherryPick.openSafeRootScope();
|
||||||
|
scope.installModules([_IntModule(1)]);
|
||||||
|
scope.dropModules();
|
||||||
|
scope.installModules([_IntModule(2)]);
|
||||||
|
|
||||||
|
expect(scope.resolve<int>(), 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Named bindings are indexed incrementally', () {
|
||||||
|
final scope = CherryPick.openSafeRootScope();
|
||||||
|
scope.installModules([_NamedIntModule('a', 1)]);
|
||||||
|
scope.installModules([_NamedIntModule('b', 2)]);
|
||||||
|
|
||||||
|
expect(scope.resolve<int>(named: 'a'), 1);
|
||||||
|
expect(scope.resolve<int>(named: 'b'), 2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user