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/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.
|
||||
@@ -60,6 +60,13 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
||||
@override
|
||||
CherryPickObserver get observer => _observer;
|
||||
|
||||
bool get _isSilentObserver => _observer is SilentCherryPickObserver;
|
||||
|
||||
bool get _canUseDirectResolvePath =>
|
||||
_isSilentObserver &&
|
||||
!isCycleDetectionEnabled &&
|
||||
!isGlobalCycleDetectionEnabled;
|
||||
|
||||
/// COLLECTS all resolved instances that implement [Disposable].
|
||||
final Set<Disposable> _disposables = HashSet();
|
||||
|
||||
@@ -71,21 +78,23 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
||||
Scope(this._parentScope, {required CherryPickObserver observer})
|
||||
: _observer = observer {
|
||||
setScopeId(_generateScopeId());
|
||||
observer.onScopeOpened(scopeId ?? 'NO_ID');
|
||||
observer.onDiagnostic(
|
||||
'Scope created: ${scopeId ?? 'NO_ID'}',
|
||||
details: {
|
||||
'type': 'Scope',
|
||||
'name': scopeId ?? 'NO_ID',
|
||||
if (_parentScope?.scopeId != null) 'parent': _parentScope!.scopeId,
|
||||
'description': 'scope created',
|
||||
},
|
||||
);
|
||||
if (!_isSilentObserver) {
|
||||
observer.onScopeOpened(scopeId ?? 'NO_ID');
|
||||
observer.onDiagnostic(
|
||||
'Scope created: ${scopeId ?? 'NO_ID'}',
|
||||
details: {
|
||||
'type': 'Scope',
|
||||
'name': scopeId ?? 'NO_ID',
|
||||
if (_parentScope?.scopeId != null) 'parent': _parentScope!.scopeId,
|
||||
'description': 'scope created',
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final Set<Module> _modulesList = HashSet();
|
||||
|
||||
// индекс для мгновенного поиска binding’ов
|
||||
// index for fast binding lookup
|
||||
final Map<Object, Map<String?, BindingResolver>> _bindingResolvers = {};
|
||||
|
||||
/// Generates a unique identifier string for this scope instance.
|
||||
@@ -119,16 +128,18 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
||||
childScope.enableGlobalCycleDetection();
|
||||
}
|
||||
_scopeMap[name] = childScope;
|
||||
observer.onDiagnostic(
|
||||
'SubScope created: $name',
|
||||
details: {
|
||||
'type': 'SubScope',
|
||||
'name': name,
|
||||
'id': childScope.scopeId,
|
||||
if (scopeId != null) 'parent': scopeId,
|
||||
'description': 'subscope created',
|
||||
},
|
||||
);
|
||||
if (!_isSilentObserver) {
|
||||
observer.onDiagnostic(
|
||||
'SubScope created: $name',
|
||||
details: {
|
||||
'type': 'SubScope',
|
||||
'name': name,
|
||||
'id': childScope.scopeId,
|
||||
if (scopeId != null) 'parent': scopeId,
|
||||
'description': 'subscope created',
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
return _scopeMap[name]!;
|
||||
}
|
||||
@@ -149,17 +160,19 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
||||
if (childScope.scopeId != null) {
|
||||
GlobalCycleDetector.instance.removeScopeDetector(childScope.scopeId!);
|
||||
}
|
||||
observer.onScopeClosed(childScope.scopeId ?? name);
|
||||
observer.onDiagnostic(
|
||||
'SubScope closed: $name',
|
||||
details: {
|
||||
'type': 'SubScope',
|
||||
'name': name,
|
||||
'id': childScope.scopeId,
|
||||
if (scopeId != null) 'parent': scopeId,
|
||||
'description': 'subscope closed',
|
||||
},
|
||||
);
|
||||
if (!_isSilentObserver) {
|
||||
observer.onScopeClosed(childScope.scopeId ?? name);
|
||||
observer.onDiagnostic(
|
||||
'SubScope closed: $name',
|
||||
details: {
|
||||
'type': 'SubScope',
|
||||
'name': name,
|
||||
'id': childScope.scopeId,
|
||||
if (scopeId != null) 'parent': scopeId,
|
||||
'description': 'subscope closed',
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
_scopeMap.remove(name);
|
||||
}
|
||||
@@ -175,30 +188,34 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
||||
/// ```
|
||||
Scope installModules(List<Module> modules) {
|
||||
_modulesList.addAll(modules);
|
||||
if (modules.isNotEmpty) {
|
||||
if (!_isSilentObserver && modules.isNotEmpty) {
|
||||
observer.onModulesInstalled(
|
||||
modules.map((m) => m.runtimeType.toString()).toList(),
|
||||
scopeName: scopeId,
|
||||
);
|
||||
}
|
||||
for (var module in modules) {
|
||||
observer.onDiagnostic(
|
||||
'Module installed: ${module.runtimeType}',
|
||||
details: {
|
||||
'type': 'Module',
|
||||
'name': module.runtimeType.toString(),
|
||||
'scope': scopeId,
|
||||
'description': 'module installed',
|
||||
},
|
||||
);
|
||||
if (!_isSilentObserver) {
|
||||
observer.onDiagnostic(
|
||||
'Module installed: ${module.runtimeType}',
|
||||
details: {
|
||||
'type': 'Module',
|
||||
'name': module.runtimeType.toString(),
|
||||
'scope': scopeId,
|
||||
'description': 'module installed',
|
||||
},
|
||||
);
|
||||
}
|
||||
module.builder(this);
|
||||
// Associate bindings with this scope's observer
|
||||
for (final binding in module.bindingSet) {
|
||||
binding.observer = observer;
|
||||
binding.logAllDeferred();
|
||||
if (!_isSilentObserver) {
|
||||
binding.logAllDeferred();
|
||||
}
|
||||
}
|
||||
_addModuleToIndex(module);
|
||||
}
|
||||
_rebuildResolversIndex();
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -212,20 +229,22 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
||||
/// testScope.dropModules();
|
||||
/// ```
|
||||
Scope dropModules() {
|
||||
if (_modulesList.isNotEmpty) {
|
||||
if (!_isSilentObserver && _modulesList.isNotEmpty) {
|
||||
observer.onModulesRemoved(
|
||||
_modulesList.map((m) => m.runtimeType.toString()).toList(),
|
||||
scopeName: scopeId,
|
||||
);
|
||||
}
|
||||
observer.onDiagnostic(
|
||||
'Modules dropped for scope: $scopeId',
|
||||
details: {
|
||||
'type': 'Scope',
|
||||
'name': scopeId,
|
||||
'description': 'modules dropped',
|
||||
},
|
||||
);
|
||||
if (!_isSilentObserver) {
|
||||
observer.onDiagnostic(
|
||||
'Modules dropped for scope: $scopeId',
|
||||
details: {
|
||||
'type': 'Scope',
|
||||
'name': scopeId,
|
||||
'description': 'modules dropped',
|
||||
},
|
||||
);
|
||||
}
|
||||
_modulesList.clear();
|
||||
_rebuildResolversIndex();
|
||||
return this;
|
||||
@@ -242,6 +261,16 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
||||
/// final special = scope.resolve<Service>(named: 'special');
|
||||
/// ```
|
||||
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);
|
||||
T result;
|
||||
if (isGlobalCycleDetectionEnabled) {
|
||||
@@ -315,6 +344,12 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
||||
/// final maybeDb = scope.tryResolve<Database>();
|
||||
/// ```
|
||||
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;
|
||||
if (isGlobalCycleDetectionEnabled) {
|
||||
result = withGlobalCycleDetection<T?>(T, named, () {
|
||||
@@ -358,6 +393,21 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
||||
/// final special = await scope.resolveAsync<Service>(named: "special");
|
||||
/// ```
|
||||
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;
|
||||
if (isGlobalCycleDetectionEnabled) {
|
||||
result = await withGlobalCycleDetection<Future<T>>(T, named, () async {
|
||||
@@ -413,6 +463,17 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
||||
/// final user = await scope.tryResolveAsync<User>();
|
||||
/// ```
|
||||
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;
|
||||
if (isGlobalCycleDetectionEnabled) {
|
||||
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.
|
||||
Future<T?> _tryResolveAsyncInternal<T>(
|
||||
{String? named, dynamic params}) async {
|
||||
Future<T?> _tryResolveAsyncInternal<T>({String? named, dynamic params}) {
|
||||
final resolver = _findBindingResolver<T>(named);
|
||||
// 1 - Try from own modules; 2 - Fallback to parent
|
||||
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.
|
||||
@@ -454,23 +515,27 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
||||
BindingResolver<T>? _findBindingResolver<T>(String? named) =>
|
||||
_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.
|
||||
/// Called after [installModules] and [dropModules]. Internal use only.
|
||||
/// Called after [dropModules]. Internal use only.
|
||||
void _rebuildResolversIndex() {
|
||||
_bindingResolvers.clear();
|
||||
for (var module in _modulesList) {
|
||||
for (var binding in module.bindingSet) {
|
||||
_bindingResolvers.putIfAbsent(binding.key, () => {});
|
||||
final nameKey = binding.isNamed ? binding.name : null;
|
||||
_bindingResolvers[binding.key]![nameKey] = binding.resolver!;
|
||||
}
|
||||
_addModuleToIndex(module);
|
||||
}
|
||||
}
|
||||
|
||||
/// Tracks resolved [Disposable] instances, to ensure dispose is called automatically.
|
||||
/// Internal use only.
|
||||
void _trackDisposable(Object? obj) {
|
||||
if (obj is Disposable && !_disposables.contains(obj)) {
|
||||
if (obj is Disposable) {
|
||||
_disposables.add(obj);
|
||||
}
|
||||
}
|
||||
@@ -487,14 +552,14 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
||||
/// ```
|
||||
Future<void> dispose() async {
|
||||
// Create copies to avoid concurrent modification
|
||||
final scopesCopy = Map<String, Scope>.from(_scopeMap);
|
||||
for (final subScope in scopesCopy.values) {
|
||||
final scopes = _scopeMap.values.toList();
|
||||
for (final subScope in scopes) {
|
||||
await subScope.dispose();
|
||||
}
|
||||
_scopeMap.clear();
|
||||
|
||||
final disposablesCopy = Set<Disposable>.from(_disposables);
|
||||
for (final d in disposablesCopy) {
|
||||
final disposables = _disposables.toList();
|
||||
for (final d in disposables) {
|
||||
await d.dispose();
|
||||
}
|
||||
_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