Compare commits

...

4 Commits

Author SHA1 Message Date
Sergey Penkovsky
298cb65ac8 chore(release): publish packages
- talker_cherrypick_logger@1.1.0-dev.4
2025-08-13 15:58:08 +03:00
Sergey Penkovsky
1b9db31c13 docs(readme): update install instructions to use pub.dev as default method and remove obsolete git example
The main installation guide now recommends pub.dev with ^latest tags. Removed the outdated GitHub install block for clarity and simplicity. No functional code changes.
2025-08-13 15:57:28 +03:00
Sergey Penkovsky
ca3cd2c8fd Merge pull request #20 from pese-git/code-format
style: reformat codebase using melos format
2025-08-13 15:46:05 +03:00
Sergey Penkovsky
c91e15319b style: reformat codebase using melos format
Applied consistent code formatting across all packages using \$ melos format
  └> dart format .
     └> RUNNING (in 8 packages)

--------------------------------------------------------------------------------
benchmark_di:
Formatted 18 files (0 changed) in 0.30 seconds.
benchmark_di: SUCCESS
--------------------------------------------------------------------------------
cherrypick:
Formatted 24 files (0 changed) in 0.34 seconds.
cherrypick: SUCCESS
--------------------------------------------------------------------------------
cherrypick_annotations:
Formatted 11 files (0 changed) in 0.14 seconds.
cherrypick_annotations: SUCCESS
--------------------------------------------------------------------------------
cherrypick_flutter:
Formatted 3 files (0 changed) in 0.15 seconds.
cherrypick_flutter: SUCCESS
--------------------------------------------------------------------------------
cherrypick_generator:
Formatted 17 files (0 changed) in 0.27 seconds.
cherrypick_generator: SUCCESS
--------------------------------------------------------------------------------
client_app:
Formatted 4 files (0 changed) in 0.14 seconds.
client_app: SUCCESS
--------------------------------------------------------------------------------
postly:
Formatted lib/router/app_router.gr.dart
Formatted 23 files (1 changed) in 0.33 seconds.
postly: SUCCESS
--------------------------------------------------------------------------------
talker_cherrypick_logger:
Formatted 4 files (0 changed) in 0.18 seconds.
talker_cherrypick_logger: SUCCESS
--------------------------------------------------------------------------------

$ melos format
  └> dart format .
     └> SUCCESS. No functional or logic changes included.
2025-08-13 15:38:44 +03:00
50 changed files with 768 additions and 470 deletions

View File

@@ -3,6 +3,27 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## 2025-08-13
### Changes
---
Packages with breaking changes:
- There are no breaking changes in this release.
Packages with other changes:
- [`talker_cherrypick_logger` - `v1.1.0-dev.4`](#talker_cherrypick_logger---v110-dev4)
---
#### `talker_cherrypick_logger` - `v1.1.0-dev.4`
- **DOCS**(readme): update install instructions to use pub.dev as default method and remove obsolete git example.
## 2025-08-13
### Changes

View File

@@ -2,4 +2,4 @@ import 'package:benchmark_di/cli/benchmark_cli.dart';
Future<void> main(List<String> args) async {
await BenchmarkCliRunner().run(args);
}
}

View File

@@ -73,7 +73,8 @@ class UniversalChainBenchmark<TContainer> extends BenchmarkBase {
_childDi!.resolve<UniversalService>();
break;
case UniversalScenario.asyncChain:
throw UnsupportedError('asyncChain supported only in UniversalChainAsyncBenchmark');
throw UnsupportedError(
'asyncChain supported only in UniversalChainAsyncBenchmark');
}
}
}

View File

@@ -36,8 +36,11 @@ class BenchmarkCliRunner {
if (config.di == 'getit') {
final di = GetItAdapter();
if (scenario == UniversalScenario.asyncChain) {
final benchAsync = UniversalChainAsyncBenchmark<GetIt>(di,
chainCount: c, nestingDepth: d, mode: mode,
final benchAsync = UniversalChainAsyncBenchmark<GetIt>(
di,
chainCount: c,
nestingDepth: d,
mode: mode,
);
benchResult = await BenchmarkRunner.runAsync(
benchmark: benchAsync,
@@ -45,8 +48,12 @@ class BenchmarkCliRunner {
repeats: config.repeats,
);
} else {
final benchSync = UniversalChainBenchmark<GetIt>(di,
chainCount: c, nestingDepth: d, mode: mode, scenario: scenario,
final benchSync = UniversalChainBenchmark<GetIt>(
di,
chainCount: c,
nestingDepth: d,
mode: mode,
scenario: scenario,
);
benchResult = await BenchmarkRunner.runSync(
benchmark: benchSync,
@@ -57,8 +64,12 @@ class BenchmarkCliRunner {
} else if (config.di == 'riverpod') {
final di = RiverpodAdapter();
if (scenario == UniversalScenario.asyncChain) {
final benchAsync = UniversalChainAsyncBenchmark<Map<String, rp.ProviderBase<Object?>>>(di,
chainCount: c, nestingDepth: d, mode: mode,
final benchAsync = UniversalChainAsyncBenchmark<
Map<String, rp.ProviderBase<Object?>>>(
di,
chainCount: c,
nestingDepth: d,
mode: mode,
);
benchResult = await BenchmarkRunner.runAsync(
benchmark: benchAsync,
@@ -66,8 +77,13 @@ class BenchmarkCliRunner {
repeats: config.repeats,
);
} else {
final benchSync = UniversalChainBenchmark<Map<String, rp.ProviderBase<Object?>>>(di,
chainCount: c, nestingDepth: d, mode: mode, scenario: scenario,
final benchSync = UniversalChainBenchmark<
Map<String, rp.ProviderBase<Object?>>>(
di,
chainCount: c,
nestingDepth: d,
mode: mode,
scenario: scenario,
);
benchResult = await BenchmarkRunner.runSync(
benchmark: benchSync,
@@ -78,8 +94,11 @@ class BenchmarkCliRunner {
} else {
final di = CherrypickDIAdapter();
if (scenario == UniversalScenario.asyncChain) {
final benchAsync = UniversalChainAsyncBenchmark<Scope>(di,
chainCount: c, nestingDepth: d, mode: mode,
final benchAsync = UniversalChainAsyncBenchmark<Scope>(
di,
chainCount: c,
nestingDepth: d,
mode: mode,
);
benchResult = await BenchmarkRunner.runAsync(
benchmark: benchAsync,
@@ -87,8 +106,12 @@ class BenchmarkCliRunner {
repeats: config.repeats,
);
} else {
final benchSync = UniversalChainBenchmark<Scope>(di,
chainCount: c, nestingDepth: d, mode: mode, scenario: scenario,
final benchSync = UniversalChainBenchmark<Scope>(
di,
chainCount: c,
nestingDepth: d,
mode: mode,
scenario: scenario,
);
benchResult = await BenchmarkRunner.runSync(
benchmark: benchSync,
@@ -103,7 +126,11 @@ class BenchmarkCliRunner {
var median = timings[timings.length ~/ 2];
var minVal = timings.first;
var maxVal = timings.last;
var stddev = timings.isEmpty ? 0 : sqrt(timings.map((x) => pow(x - mean, 2)).reduce((a, b) => a + b) / timings.length);
var stddev = timings.isEmpty
? 0
: sqrt(
timings.map((x) => pow(x - mean, 2)).reduce((a, b) => a + b) /
timings.length);
results.add({
'benchmark': 'Universal_$bench',
'chainCount': c,
@@ -128,6 +155,7 @@ class BenchmarkCliRunner {
'json': JsonReport(),
'markdown': MarkdownReport(),
};
print(reportGenerators[config.format]?.render(results) ?? PrettyReport().render(results));
print(reportGenerators[config.format]?.render(results) ??
PrettyReport().render(results));
}
}
}

View File

@@ -8,14 +8,19 @@ import 'package:benchmark_di/scenarios/universal_scenario.dart';
enum UniversalBenchmark {
/// Simple singleton registration benchmark
registerSingleton,
/// Chain of singleton dependencies
chainSingleton,
/// Chain using factories
chainFactory,
/// Async chain resolution
chainAsync,
/// Named registration benchmark
named,
/// Override/child-scope benchmark
override,
}
@@ -65,23 +70,32 @@ T parseEnum<T>(String value, List<T> values, T defaultValue) {
}
/// Parses comma-separated integer list from [s].
List<int> parseIntList(String s) =>
s.split(',').map((e) => int.tryParse(e.trim()) ?? 0).where((x) => x > 0).toList();
List<int> parseIntList(String s) => s
.split(',')
.map((e) => int.tryParse(e.trim()) ?? 0)
.where((x) => x > 0)
.toList();
/// CLI config describing what and how to benchmark.
class BenchmarkCliConfig {
/// Benchmarks enabled to run (scenarios).
final List<UniversalBenchmark> benchesToRun;
/// List of chain counts (parallel, per test).
final List<int> chainCounts;
/// List of nesting depths (max chain length, per test).
final List<int> nestDepths;
/// How many times to repeat each trial.
final int repeats;
/// How many times to warm-up before measuring.
final int warmups;
/// Output report format.
final String format;
/// Name of DI implementation ("cherrypick" or "getit")
final String di;
BenchmarkCliConfig({
@@ -105,7 +119,9 @@ BenchmarkCliConfig parseBenchmarkCli(List<String> args) {
..addOption('repeat', abbr: 'r', defaultsTo: '2')
..addOption('warmup', abbr: 'w', defaultsTo: '1')
..addOption('format', abbr: 'f', defaultsTo: 'pretty')
..addOption('di', defaultsTo: 'cherrypick', help: 'DI implementation: cherrypick, getit or riverpod')
..addOption('di',
defaultsTo: 'cherrypick',
help: 'DI implementation: cherrypick, getit or riverpod')
..addFlag('help', abbr: 'h', negatable: false, help: 'Show help');
final result = parser.parse(args);
if (result['help'] == true) {
@@ -127,4 +143,4 @@ BenchmarkCliConfig parseBenchmarkCli(List<String> args) {
format: result['format'] as String,
di: result['di'] as String? ?? 'cherrypick',
);
}
}

View File

@@ -5,20 +5,32 @@ class CsvReport extends ReportGenerator {
/// List of all keys/columns to include in the CSV output.
@override
final List<String> keys = [
'benchmark','chainCount','nestingDepth','mean_us','median_us','stddev_us',
'min_us','max_us','trials','timings_us','memory_diff_kb','delta_peak_kb','peak_rss_kb'
'benchmark',
'chainCount',
'nestingDepth',
'mean_us',
'median_us',
'stddev_us',
'min_us',
'max_us',
'trials',
'timings_us',
'memory_diff_kb',
'delta_peak_kb',
'peak_rss_kb'
];
/// Renders rows as a CSV table string.
@override
String render(List<Map<String, dynamic>> rows) {
final header = keys.join(',');
final lines = rows.map((r) =>
keys.map((k) {
final v = r[k];
if (v is List) return '"${v.join(';')}"';
return (v ?? '').toString();
}).join(',')
).toList();
final lines = rows
.map((r) => keys.map((k) {
final v = r[k];
if (v is List) return '"${v.join(';')}"';
return (v ?? '').toString();
}).join(','))
.toList();
return ([header] + lines).join('\n');
}
}
}

View File

@@ -5,9 +5,10 @@ class JsonReport extends ReportGenerator {
/// No specific keys; outputs all fields in raw map.
@override
List<String> get keys => [];
/// Renders all result rows as a pretty-printed JSON array.
@override
String render(List<Map<String, dynamic>> rows) {
return '[\n${rows.map((r) => ' $r').join(',\n')}\n]';
}
}
}

View File

@@ -7,25 +7,46 @@ class MarkdownReport extends ReportGenerator {
/// List of columns (keys) to show in the Markdown table.
@override
final List<String> keys = [
'benchmark','chainCount','nestingDepth','mean_us','median_us','stddev_us',
'min_us','max_us','trials','memory_diff_kb','delta_peak_kb','peak_rss_kb'
'benchmark',
'chainCount',
'nestingDepth',
'mean_us',
'median_us',
'stddev_us',
'min_us',
'max_us',
'trials',
'memory_diff_kb',
'delta_peak_kb',
'peak_rss_kb'
];
/// Friendly display names for each benchmark type.
static const nameMap = {
'Universal_UniversalBenchmark.registerSingleton':'RegisterSingleton',
'Universal_UniversalBenchmark.chainSingleton':'ChainSingleton',
'Universal_UniversalBenchmark.chainFactory':'ChainFactory',
'Universal_UniversalBenchmark.chainAsync':'AsyncChain',
'Universal_UniversalBenchmark.named':'Named',
'Universal_UniversalBenchmark.override':'Override',
'Universal_UniversalBenchmark.registerSingleton': 'RegisterSingleton',
'Universal_UniversalBenchmark.chainSingleton': 'ChainSingleton',
'Universal_UniversalBenchmark.chainFactory': 'ChainFactory',
'Universal_UniversalBenchmark.chainAsync': 'AsyncChain',
'Universal_UniversalBenchmark.named': 'Named',
'Universal_UniversalBenchmark.override': 'Override',
};
/// Renders all results as a formatted Markdown table with aligned columns and a legend.
@override
String render(List<Map<String, dynamic>> rows) {
final headers = [
'Benchmark', 'Chain Count', 'Depth', 'Mean (us)', 'Median', 'Stddev', 'Min', 'Max', 'N', 'ΔRSS(KB)', 'ΔPeak(KB)', 'PeakRSS(KB)'
'Benchmark',
'Chain Count',
'Depth',
'Mean (us)',
'Median',
'Stddev',
'Min',
'Max',
'N',
'ΔRSS(KB)',
'ΔPeak(KB)',
'PeakRSS(KB)'
];
final dataRows = rows.map((r) {
final readableName = nameMap[r['benchmark']] ?? r['benchmark'];
@@ -73,6 +94,6 @@ class MarkdownReport extends ReportGenerator {
> `PeakRSS(KB)` Max observed RSS memory (KB)
''';
return '$legend\n\n${([headerLine, divider] + lines).join('\n')}' ;
return '$legend\n\n${([headerLine, divider] + lines).join('\n')}';
}
}
}

View File

@@ -7,25 +7,46 @@ class PrettyReport extends ReportGenerator {
/// List of columns to output in the pretty report.
@override
final List<String> keys = [
'benchmark','chainCount','nestingDepth','mean_us','median_us','stddev_us',
'min_us','max_us','trials','memory_diff_kb','delta_peak_kb','peak_rss_kb'
'benchmark',
'chainCount',
'nestingDepth',
'mean_us',
'median_us',
'stddev_us',
'min_us',
'max_us',
'trials',
'memory_diff_kb',
'delta_peak_kb',
'peak_rss_kb'
];
/// Mappings from internal benchmark IDs to display names.
static const nameMap = {
'Universal_UniversalBenchmark.registerSingleton': 'RegisterSingleton',
'Universal_UniversalBenchmark.chainSingleton': 'ChainSingleton',
'Universal_UniversalBenchmark.chainFactory': 'ChainFactory',
'Universal_UniversalBenchmark.chainAsync': 'AsyncChain',
'Universal_UniversalBenchmark.named': 'Named',
'Universal_UniversalBenchmark.override': 'Override',
'Universal_UniversalBenchmark.registerSingleton': 'RegisterSingleton',
'Universal_UniversalBenchmark.chainSingleton': 'ChainSingleton',
'Universal_UniversalBenchmark.chainFactory': 'ChainFactory',
'Universal_UniversalBenchmark.chainAsync': 'AsyncChain',
'Universal_UniversalBenchmark.named': 'Named',
'Universal_UniversalBenchmark.override': 'Override',
};
/// Renders the results as a header + tab-separated value table.
@override
String render(List<Map<String, dynamic>> rows) {
final headers = [
'Benchmark', 'Chain Count', 'Depth', 'Mean (us)', 'Median', 'Stddev', 'Min', 'Max', 'N', 'ΔRSS(KB)', 'ΔPeak(KB)', 'PeakRSS(KB)'
'Benchmark',
'Chain Count',
'Depth',
'Mean (us)',
'Median',
'Stddev',
'Min',
'Max',
'N',
'ΔRSS(KB)',
'ΔPeak(KB)',
'PeakRSS(KB)'
];
final header = headers.join('\t');
final lines = rows.map((r) {

View File

@@ -4,6 +4,7 @@
abstract class ReportGenerator {
/// Renders the given [results] as a formatted string (table, markdown, csv, etc).
String render(List<Map<String, dynamic>> results);
/// List of output columns/keys included in the export (or [] for auto/all).
List<String> get keys;
}
}

View File

@@ -7,10 +7,13 @@ import 'package:benchmark_di/benchmarks/universal_chain_async_benchmark.dart';
class BenchmarkResult {
/// List of timings for each run (in microseconds).
final List<num> timings;
/// Difference in memory (RSS, in KB) after running.
final int memoryDiffKb;
/// Difference between peak RSS and initial RSS (in KB).
final int deltaPeakKb;
/// Peak RSS memory observed (in KB).
final int peakRssKb;
BenchmarkResult({
@@ -19,6 +22,7 @@ class BenchmarkResult {
required this.deltaPeakKb,
required this.peakRssKb,
});
/// Computes a BenchmarkResult instance from run timings and memory data.
factory BenchmarkResult.collect({
required List<num> timings,
@@ -64,7 +68,8 @@ class BenchmarkRunner {
rssValues.add(ProcessInfo.currentRss);
benchmark.teardown();
}
return BenchmarkResult.collect(timings: timings, rssValues: rssValues, memBefore: memBefore);
return BenchmarkResult.collect(
timings: timings, rssValues: rssValues, memBefore: memBefore);
}
/// Runs an asynchronous benchmark ([UniversalChainAsyncBenchmark]) for a given number of [warmups] and [repeats].
@@ -91,6 +96,7 @@ class BenchmarkRunner {
rssValues.add(ProcessInfo.currentRss);
await benchmark.teardown();
}
return BenchmarkResult.collect(timings: timings, rssValues: rssValues, memBefore: memBefore);
return BenchmarkResult.collect(
timings: timings, rssValues: rssValues, memBefore: memBefore);
}
}
}

View File

@@ -4,7 +4,6 @@ import 'package:benchmark_di/scenarios/universal_service.dart';
import 'package:cherrypick/cherrypick.dart';
import 'di_adapter.dart';
/// Test module that generates a chain of service bindings for benchmarking.
///
/// Configurable by chain count, nesting depth, binding mode, and scenario
@@ -12,10 +11,13 @@ import 'di_adapter.dart';
class UniversalChainModule extends Module {
/// Number of chains to create.
final int chainCount;
/// Depth of each chain.
final int nestingDepth;
/// How modules are registered (factory/singleton/async).
final UniversalBindingMode bindingMode;
/// Which di scenario to generate (chained, named, etc).
final UniversalScenario scenario;
@@ -38,17 +40,18 @@ class UniversalChainModule extends Module {
final prevDepName = '${chain}_${level - 1}';
final depName = '${chain}_$level';
bind<UniversalService>()
.toProvideAsync(() async {
final prev = level > 1
? await currentScope.resolveAsync<UniversalService>(named: prevDepName)
: null;
return UniversalServiceImpl(
value: depName,
dependency: prev,
);
})
.withName(depName)
.singleton();
.toProvideAsync(() async {
final prev = level > 1
? await currentScope.resolveAsync<UniversalService>(
named: prevDepName)
: null;
return UniversalServiceImpl(
value: depName,
dependency: prev,
);
})
.withName(depName)
.singleton();
}
}
return;
@@ -58,13 +61,18 @@ class UniversalChainModule extends Module {
case UniversalScenario.register:
// Simple singleton registration.
bind<UniversalService>()
.toProvide(() => UniversalServiceImpl(value: 'reg', dependency: null))
.toProvide(
() => UniversalServiceImpl(value: 'reg', dependency: null))
.singleton();
break;
case UniversalScenario.named:
// Named factory registration for two distinct objects.
bind<UniversalService>().toProvide(() => UniversalServiceImpl(value: 'impl1')).withName('impl1');
bind<UniversalService>().toProvide(() => UniversalServiceImpl(value: 'impl2')).withName('impl2');
bind<UniversalService>()
.toProvide(() => UniversalServiceImpl(value: 'impl1'))
.withName('impl1');
bind<UniversalService>()
.toProvide(() => UniversalServiceImpl(value: 'impl2'))
.withName('impl2');
break;
case UniversalScenario.chain:
// Chain of nested services, with dependency on previous level by name.
@@ -79,7 +87,8 @@ class UniversalChainModule extends Module {
bind<UniversalService>()
.toProvide(() => UniversalServiceImpl(
value: depName,
dependency: currentScope.tryResolve<UniversalService>(named: prevDepName),
dependency: currentScope.tryResolve<UniversalService>(
named: prevDepName),
))
.withName(depName)
.singleton();
@@ -88,7 +97,8 @@ class UniversalChainModule extends Module {
bind<UniversalService>()
.toProvide(() => UniversalServiceImpl(
value: depName,
dependency: currentScope.tryResolve<UniversalService>(named: prevDepName),
dependency: currentScope.tryResolve<UniversalService>(
named: prevDepName),
))
.withName(depName);
break;
@@ -96,7 +106,9 @@ class UniversalChainModule extends Module {
bind<UniversalService>()
.toProvideAsync(() async => UniversalServiceImpl(
value: depName,
dependency: await currentScope.resolveAsync<UniversalService>(named: prevDepName),
dependency:
await currentScope.resolveAsync<UniversalService>(
named: prevDepName),
))
.withName(depName)
.singleton();
@@ -107,14 +119,16 @@ class UniversalChainModule extends Module {
// Регистрация алиаса без имени (на последний элемент цепочки)
final depName = '${chainCount}_$nestingDepth';
bind<UniversalService>()
.toProvide(() => currentScope.resolve<UniversalService>(named: depName))
.toProvide(
() => currentScope.resolve<UniversalService>(named: depName))
.singleton();
break;
case UniversalScenario.override:
// handled at benchmark level, но алиас нужен прямо в этом scope!
final depName = '${chainCount}_$nestingDepth';
bind<UniversalService>()
.toProvide(() => currentScope.resolve<UniversalService>(named: depName))
.toProvide(
() => currentScope.resolve<UniversalService>(named: depName))
.singleton();
break;
case UniversalScenario.asyncChain:
@@ -124,7 +138,6 @@ class UniversalChainModule extends Module {
}
}
class CherrypickDIAdapter extends DIAdapter<Scope> {
Scope? _scope;
final bool _isSubScope;
@@ -158,7 +171,8 @@ class CherrypickDIAdapter extends DIAdapter<Scope> {
]);
};
}
throw UnsupportedError('Scenario $scenario not supported by CherrypickDIAdapter');
throw UnsupportedError(
'Scenario $scenario not supported by CherrypickDIAdapter');
}
@override

View File

@@ -1,4 +1,5 @@
import 'package:benchmark_di/scenarios/universal_binding_mode.dart';
/// Универсальная абстракция для DI-адаптера с унифицированной функцией регистрации.
/// Теперь для каждого адаптера задаём строгий generic тип контейнера.
typedef Registration<TContainer> = void Function(TContainer);

View File

@@ -80,9 +80,11 @@ class GetItAdapter extends DIAdapter<GetIt> {
getIt.registerSingletonAsync<UniversalService>(
() async {
final prev = level > 1
? await getIt.getAsync<UniversalService>(instanceName: prevDepName)
? await getIt.getAsync<UniversalService>(
instanceName: prevDepName)
: null;
return UniversalServiceImpl(value: depName, dependency: prev);
return UniversalServiceImpl(
value: depName, dependency: prev);
},
instanceName: depName,
);
@@ -90,11 +92,16 @@ class GetItAdapter extends DIAdapter<GetIt> {
}
break;
case UniversalScenario.register:
getIt.registerSingleton<UniversalService>(UniversalServiceImpl(value: 'reg', dependency: null));
getIt.registerSingleton<UniversalService>(
UniversalServiceImpl(value: 'reg', dependency: null));
break;
case UniversalScenario.named:
getIt.registerFactory<UniversalService>(() => UniversalServiceImpl(value: 'impl1'), instanceName: 'impl1');
getIt.registerFactory<UniversalService>(() => UniversalServiceImpl(value: 'impl2'), instanceName: 'impl2');
getIt.registerFactory<UniversalService>(
() => UniversalServiceImpl(value: 'impl1'),
instanceName: 'impl1');
getIt.registerFactory<UniversalService>(
() => UniversalServiceImpl(value: 'impl2'),
instanceName: 'impl2');
break;
case UniversalScenario.chain:
for (int chain = 1; chain <= chainCount; chain++) {
@@ -107,8 +114,8 @@ class GetItAdapter extends DIAdapter<GetIt> {
UniversalServiceImpl(
value: depName,
dependency: level > 1
? getIt<UniversalService>(instanceName: prevDepName)
: null,
? getIt<UniversalService>(instanceName: prevDepName)
: null,
),
instanceName: depName,
);
@@ -129,8 +136,9 @@ class GetItAdapter extends DIAdapter<GetIt> {
() async => UniversalServiceImpl(
value: depName,
dependency: level > 1
? await getIt.getAsync<UniversalService>(instanceName: prevDepName)
: null,
? await getIt.getAsync<UniversalService>(
instanceName: prevDepName)
: null,
),
instanceName: depName,
);
@@ -143,7 +151,8 @@ class GetItAdapter extends DIAdapter<GetIt> {
// handled at benchmark level
break;
}
if (scenario == UniversalScenario.chain || scenario == UniversalScenario.override) {
if (scenario == UniversalScenario.chain ||
scenario == UniversalScenario.override) {
final depName = '${chainCount}_$nestingDepth';
getIt.registerSingleton<UniversalService>(
getIt<UniversalService>(instanceName: depName),

View File

@@ -20,7 +20,9 @@ class RiverpodAdapter extends DIAdapter<Map<String, rp.ProviderBase<Object?>>> {
_parent = parent;
@override
void setupDependencies(void Function(Map<String, rp.ProviderBase<Object?>> container) registration) {
void setupDependencies(
void Function(Map<String, rp.ProviderBase<Object?>> container)
registration) {
_container ??= _parent == null
? rp.ProviderContainer()
: rp.ProviderContainer(parent: _parent);
@@ -76,7 +78,8 @@ class RiverpodAdapter extends DIAdapter<Map<String, rp.ProviderBase<Object?>>> {
}
@override
Registration<Map<String, rp.ProviderBase<Object?>>> universalRegistration<S extends Enum>({
Registration<Map<String, rp.ProviderBase<Object?>>>
universalRegistration<S extends Enum>({
required S scenario,
required int chainCount,
required int nestingDepth,
@@ -86,25 +89,34 @@ class RiverpodAdapter extends DIAdapter<Map<String, rp.ProviderBase<Object?>>> {
return (providers) {
switch (scenario) {
case UniversalScenario.register:
providers['UniversalService'] = rp.Provider<UniversalService>((ref) => UniversalServiceImpl(value: 'reg', dependency: null));
providers['UniversalService'] = rp.Provider<UniversalService>(
(ref) => UniversalServiceImpl(value: 'reg', dependency: null));
break;
case UniversalScenario.named:
providers['impl1'] = rp.Provider<UniversalService>((ref) => UniversalServiceImpl(value: 'impl1'));
providers['impl2'] = rp.Provider<UniversalService>((ref) => UniversalServiceImpl(value: 'impl2'));
providers['impl1'] = rp.Provider<UniversalService>(
(ref) => UniversalServiceImpl(value: 'impl1'));
providers['impl2'] = rp.Provider<UniversalService>(
(ref) => UniversalServiceImpl(value: 'impl2'));
break;
case UniversalScenario.chain:
for (int chain = 1; chain <= chainCount; chain++) {
for (int level = 1; level <= nestingDepth; level++) {
final prevDepName = '${chain}_${level - 1}';
final depName = '${chain}_$level';
providers[depName] = rp.Provider<UniversalService>((ref) => UniversalServiceImpl(
value: depName,
dependency: level > 1 ? ref.watch(providers[prevDepName] as rp.ProviderBase<UniversalService>) : null,
));
providers[depName] =
rp.Provider<UniversalService>((ref) => UniversalServiceImpl(
value: depName,
dependency: level > 1
? ref.watch(providers[prevDepName]
as rp.ProviderBase<UniversalService>)
: null,
));
}
}
final depName = '${chainCount}_$nestingDepth';
providers['UniversalService'] = rp.Provider<UniversalService>((ref) => ref.watch(providers[depName] as rp.ProviderBase<UniversalService>));
providers['UniversalService'] = rp.Provider<UniversalService>(
(ref) => ref.watch(
providers[depName] as rp.ProviderBase<UniversalService>));
break;
case UniversalScenario.override:
// handled at benchmark level
@@ -114,24 +126,31 @@ class RiverpodAdapter extends DIAdapter<Map<String, rp.ProviderBase<Object?>>> {
for (int level = 1; level <= nestingDepth; level++) {
final prevDepName = '${chain}_${level - 1}';
final depName = '${chain}_$level';
providers[depName] = rp.FutureProvider<UniversalService>((ref) async {
providers[depName] =
rp.FutureProvider<UniversalService>((ref) async {
return UniversalServiceImpl(
value: depName,
dependency: level > 1
? await ref.watch((providers[prevDepName] as rp.FutureProvider<UniversalService>).future) as UniversalService?
? await ref.watch((providers[prevDepName]
as rp.FutureProvider<UniversalService>)
.future) as UniversalService?
: null,
);
});
}
}
final depName = '${chainCount}_$nestingDepth';
providers['UniversalService'] = rp.FutureProvider<UniversalService>((ref) async {
return await ref.watch((providers[depName] as rp.FutureProvider<UniversalService>).future);
providers['UniversalService'] =
rp.FutureProvider<UniversalService>((ref) async {
return await ref.watch(
(providers[depName] as rp.FutureProvider<UniversalService>)
.future);
});
break;
}
};
}
throw UnsupportedError('Scenario $scenario not supported by RiverpodAdapter');
throw UnsupportedError(
'Scenario $scenario not supported by RiverpodAdapter');
}
}

View File

@@ -2,12 +2,16 @@
enum UniversalScenario {
/// Single registration.
register,
/// Chain of dependencies.
chain,
/// Named registrations.
named,
/// Child-scope override scenario.
override,
/// Asynchronous chain scenario.
asyncChain,
}

View File

@@ -1,4 +1,3 @@
/// Base interface for any universal service in the benchmarks.
///
/// Represents an object in the dependency chain with an identifiable value
@@ -6,6 +5,7 @@
abstract class UniversalService {
/// String ID for this service instance (e.g. chain/level info).
final String value;
/// Optional reference to dependency service in the chain.
final UniversalService? dependency;
UniversalService({required this.value, this.dependency});
@@ -14,4 +14,4 @@ abstract class UniversalService {
/// Default implementation for [UniversalService] used in service chains.
class UniversalServiceImpl extends UniversalService {
UniversalServiceImpl({required super.value, super.dependency});
}
}

View File

@@ -47,7 +47,7 @@ packages:
path: "../cherrypick"
relative: true
source: path
version: "3.0.0-dev.8"
version: "3.0.0-dev.9"
collection:
dependency: transitive
description:

View File

@@ -47,19 +47,19 @@ class FeatureModule extends Module {
Future<void> main() async {
try {
final scope = CherryPick.openRootScope().installModules([AppModule()]);
final scope = CherryPick.openRootScope().installModules([AppModule()]);
final subScope = scope
.openSubScope("featureScope")
.installModules([FeatureModule(isMock: true)]);
// Asynchronous instance resolution
final dataBloc = await subScope.resolveAsync<DataBloc>();
dataBloc.data.listen(
(d) => print('Received data: $d'),
onError: (e) => print('Error: $e'),
onDone: () => print('DONE'),
);
// Asynchronous instance resolution
final dataBloc = await subScope.resolveAsync<DataBloc>();
dataBloc.data.listen(
(d) => print('Received data: $d'),
onError: (e) => print('Error: $e'),
onDone: () => print('DONE'),
);
await dataBloc.fetchData();
} catch (e) {

View File

@@ -8,7 +8,7 @@ class DatabaseService {
class ApiService {
final DatabaseService database;
ApiService(this.database);
void fetchData() {
database.connect();
print('📡 Fetching data via API');
@@ -18,7 +18,7 @@ class ApiService {
class UserService {
final ApiService apiService;
UserService(this.apiService);
void getUser(String id) {
apiService.fetchData();
print('👤 Fetching user: $id');
@@ -36,18 +36,16 @@ class DatabaseModule extends Module {
class ApiModule extends Module {
@override
void builder(Scope currentScope) {
bind<ApiService>().toProvide(() => ApiService(
currentScope.resolve<DatabaseService>()
));
bind<ApiService>()
.toProvide(() => ApiService(currentScope.resolve<DatabaseService>()));
}
}
class UserModule extends Module {
@override
void builder(Scope currentScope) {
bind<UserService>().toProvide(() => UserService(
currentScope.resolve<ApiService>()
));
bind<UserService>()
.toProvide(() => UserService(currentScope.resolve<ApiService>()));
}
}
@@ -65,74 +63,75 @@ class CircularServiceB {
class CircularModuleA extends Module {
@override
void builder(Scope currentScope) {
bind<CircularServiceA>().toProvide(() => CircularServiceA(
currentScope.resolve<CircularServiceB>()
));
bind<CircularServiceA>().toProvide(
() => CircularServiceA(currentScope.resolve<CircularServiceB>()));
}
}
class CircularModuleB extends Module {
@override
void builder(Scope currentScope) {
bind<CircularServiceB>().toProvide(() => CircularServiceB(
currentScope.resolve<CircularServiceA>()
));
bind<CircularServiceB>().toProvide(
() => CircularServiceB(currentScope.resolve<CircularServiceA>()));
}
}
void main() {
print('=== Improved CherryPick Helper Demonstration ===\n');
// Example 1: Global enabling of cycle detection
print('1. Globally enable cycle detection:');
CherryPick.enableGlobalCycleDetection();
print('✅ Global cycle detection enabled: ${CherryPick.isGlobalCycleDetectionEnabled}');
print(
'✅ Global cycle detection enabled: ${CherryPick.isGlobalCycleDetectionEnabled}');
// All new scopes will automatically have cycle detection enabled
final globalScope = CherryPick.openRootScope();
print('✅ Root scope has cycle detection enabled: ${globalScope.isCycleDetectionEnabled}');
print(
'✅ Root scope has cycle detection enabled: ${globalScope.isCycleDetectionEnabled}');
// Install modules without circular dependencies
globalScope.installModules([
DatabaseModule(),
ApiModule(),
UserModule(),
]);
final userService = globalScope.resolve<UserService>();
userService.getUser('user123');
print('');
// Example 2: Safe scope creation
print('2. Creating safe scopes:');
CherryPick.closeRootScope(); // Закрываем предыдущий скоуп
CherryPick.disableGlobalCycleDetection(); // Отключаем глобальную настройку
// Создаем безопасный скоуп (с автоматически включенным обнаружением)
final safeScope = CherryPick.openSafeRootScope();
print('✅ Safe scope created with cycle detection: ${safeScope.isCycleDetectionEnabled}');
print(
'✅ Safe scope created with cycle detection: ${safeScope.isCycleDetectionEnabled}');
safeScope.installModules([
DatabaseModule(),
ApiModule(),
UserModule(),
]);
final safeUserService = safeScope.resolve<UserService>();
safeUserService.getUser('safe_user456');
print('');
// Example 3: Detecting cycles
print('3. Detecting circular dependencies:');
final cyclicScope = CherryPick.openSafeRootScope();
cyclicScope.installModules([
CircularModuleA(),
CircularModuleB(),
]);
try {
cyclicScope.resolve<CircularServiceA>();
print('❌ This should not be executed');
@@ -144,87 +143,96 @@ void main() {
}
}
print('');
// Example 4: Managing detection for specific scopes
print('4. Managing detection for specific scopes:');
CherryPick.closeRootScope();
// Создаем скоуп без обнаружения
// ignore: unused_local_variable
final specificScope = CherryPick.openRootScope();
print(' Detection in root scope: ${CherryPick.isCycleDetectionEnabledForScope()}');
print(
' Detection in root scope: ${CherryPick.isCycleDetectionEnabledForScope()}');
// Включаем обнаружение для конкретного скоупа
CherryPick.enableCycleDetectionForScope();
print('✅ Detection enabled for root scope: ${CherryPick.isCycleDetectionEnabledForScope()}');
print(
'✅ Detection enabled for root scope: ${CherryPick.isCycleDetectionEnabledForScope()}');
// Создаем дочерний скоуп
// ignore: unused_local_variable
final featureScope = CherryPick.openScope(scopeName: 'feature.auth');
print(' Detection in feature.auth scope: ${CherryPick.isCycleDetectionEnabledForScope(scopeName: 'feature.auth')}');
print(
' Detection in feature.auth scope: ${CherryPick.isCycleDetectionEnabledForScope(scopeName: 'feature.auth')}');
// Включаем обнаружение для дочернего скоупа
CherryPick.enableCycleDetectionForScope(scopeName: 'feature.auth');
print('✅ Detection enabled for feature.auth scope: ${CherryPick.isCycleDetectionEnabledForScope(scopeName: 'feature.auth')}');
print(
'✅ Detection enabled for feature.auth scope: ${CherryPick.isCycleDetectionEnabledForScope(scopeName: 'feature.auth')}');
print('');
// Example 5: Creating safe child scopes
print('5. Creating safe child scopes:');
final safeFeatureScope = CherryPick.openSafeScope(scopeName: 'feature.payments');
print('✅ Safe feature scope created: ${safeFeatureScope.isCycleDetectionEnabled}');
final safeFeatureScope =
CherryPick.openSafeScope(scopeName: 'feature.payments');
print(
'✅ Safe feature scope created: ${safeFeatureScope.isCycleDetectionEnabled}');
// You can create a complex hierarchy of scopes
final complexScope = CherryPick.openSafeScope(scopeName: 'app.feature.auth.login');
final complexScope =
CherryPick.openSafeScope(scopeName: 'app.feature.auth.login');
print('✅ Complex scope created: ${complexScope.isCycleDetectionEnabled}');
print('');
// Example 6: Tracking resolution chains
print('6. Tracking dependency resolution chains:');
final trackingScope = CherryPick.openSafeRootScope();
trackingScope.installModules([
DatabaseModule(),
ApiModule(),
UserModule(),
]);
print(' Chain before resolve: ${CherryPick.getCurrentResolutionChain()}');
// The chain is populated during resolution, but cleared after completion
// ignore: unused_local_variable
final trackedUserService = trackingScope.resolve<UserService>();
print(' Chain after resolve: ${CherryPick.getCurrentResolutionChain()}');
print('');
// Example 7: Usage recommendations
print('7. Recommended usage:');
print('');
print('🔧 Development mode:');
print(' CherryPick.enableGlobalCycleDetection(); // Enable globally');
print(' or');
print(' final scope = CherryPick.openSafeRootScope(); // Safe scope');
print('');
print('🚀 Production mode:');
print(' CherryPick.disableGlobalCycleDetection(); // Disable for performance');
print(
' CherryPick.disableGlobalCycleDetection(); // Disable for performance');
print(' final scope = CherryPick.openRootScope(); // Regular scope');
print('');
print('🧪 Testing:');
print(' setUp(() => CherryPick.enableGlobalCycleDetection());');
print(' tearDown(() => CherryPick.closeRootScope());');
print('');
print('🎯 Feature-specific:');
print(' CherryPick.enableCycleDetectionForScope(scopeName: "feature.critical");');
print(
' CherryPick.enableCycleDetectionForScope(scopeName: "feature.critical");');
print(' // Enable only for critical features');
// Cleanup
CherryPick.closeRootScope();
CherryPick.disableGlobalCycleDetection();
print('\n=== Demonstration complete ===');
}

View File

@@ -3,9 +3,9 @@ import 'package:cherrypick/cherrypick.dart';
// Пример сервисов с циклической зависимостью
class UserService {
final OrderService orderService;
UserService(this.orderService);
void createUser(String name) {
print('Creating user: $name');
// Пытаемся получить заказы пользователя, что создает циклическую зависимость
@@ -15,9 +15,9 @@ class UserService {
class OrderService {
final UserService userService;
OrderService(this.userService);
void getOrdersForUser(String userName) {
print('Getting orders for user: $userName');
// Пытаемся получить информацию о пользователе, что создает циклическую зависимость
@@ -29,18 +29,16 @@ class OrderService {
class UserModule extends Module {
@override
void builder(Scope currentScope) {
bind<UserService>().toProvide(() => UserService(
currentScope.resolve<OrderService>()
));
bind<UserService>()
.toProvide(() => UserService(currentScope.resolve<OrderService>()));
}
}
class OrderModule extends Module {
@override
void builder(Scope currentScope) {
bind<OrderService>().toProvide(() => OrderService(
currentScope.resolve<UserService>()
));
bind<OrderService>()
.toProvide(() => OrderService(currentScope.resolve<UserService>()));
}
}
@@ -49,7 +47,7 @@ class UserRepository {
void createUser(String name) {
print('Creating user in repository: $name');
}
String getUserInfo(String name) {
return 'User info for: $name';
}
@@ -59,7 +57,7 @@ class OrderRepository {
void createOrder(String orderId, String userName) {
print('Creating order $orderId for user: $userName');
}
List<String> getOrdersForUser(String userName) {
return ['order1', 'order2', 'order3'];
}
@@ -67,13 +65,13 @@ class OrderRepository {
class ImprovedUserService {
final UserRepository userRepository;
ImprovedUserService(this.userRepository);
void createUser(String name) {
userRepository.createUser(name);
}
String getUserInfo(String name) {
return userRepository.getUserInfo(name);
}
@@ -82,17 +80,17 @@ class ImprovedUserService {
class ImprovedOrderService {
final OrderRepository orderRepository;
final ImprovedUserService userService;
ImprovedOrderService(this.orderRepository, this.userService);
void createOrder(String orderId, String userName) {
// Проверяем, что пользователь существует
final userInfo = userService.getUserInfo(userName);
print('User exists: $userInfo');
orderRepository.createOrder(orderId, userName);
}
List<String> getOrdersForUser(String userName) {
return orderRepository.getOrdersForUser(userName);
}
@@ -103,9 +101,8 @@ class ImprovedUserModule extends Module {
@override
void builder(Scope currentScope) {
bind<UserRepository>().singleton().toProvide(() => UserRepository());
bind<ImprovedUserService>().toProvide(() => ImprovedUserService(
currentScope.resolve<UserRepository>()
));
bind<ImprovedUserService>().toProvide(
() => ImprovedUserService(currentScope.resolve<UserRepository>()));
}
}
@@ -114,81 +111,80 @@ class ImprovedOrderModule extends Module {
void builder(Scope currentScope) {
bind<OrderRepository>().singleton().toProvide(() => OrderRepository());
bind<ImprovedOrderService>().toProvide(() => ImprovedOrderService(
currentScope.resolve<OrderRepository>(),
currentScope.resolve<ImprovedUserService>()
));
currentScope.resolve<OrderRepository>(),
currentScope.resolve<ImprovedUserService>()));
}
}
void main() {
print('=== Circular Dependency Detection Example ===\n');
// Example 1: Demonstrate circular dependency
print('1. Attempt to create a scope with circular dependencies:');
try {
final scope = CherryPick.openRootScope();
scope.enableCycleDetection(); // Включаем обнаружение циклических зависимостей
scope
.enableCycleDetection(); // Включаем обнаружение циклических зависимостей
scope.installModules([
UserModule(),
OrderModule(),
]);
// Это должно выбросить CircularDependencyException
final userService = scope.resolve<UserService>();
print('UserService created: $userService');
} catch (e) {
print('❌ Circular dependency detected: $e\n');
}
// Example 2: Without circular dependency detection (dangerous!)
print('2. Same code without circular dependency detection:');
try {
final scope = CherryPick.openRootScope();
// НЕ включаем обнаружение циклических зависимостей
scope.installModules([
UserModule(),
OrderModule(),
]);
// Это приведет к StackOverflowError при попытке использования
final userService = scope.resolve<UserService>();
print('UserService создан: $userService');
// Попытка использовать сервис приведет к бесконечной рекурсии
// userService.createUser('John'); // Раскомментируйте для демонстрации StackOverflow
print('⚠️ UserService created, but using it will cause StackOverflow\n');
} catch (e) {
print('❌ Error: $e\n');
}
// Example 3: Correct architecture without circular dependencies
print('3. Correct architecture without circular dependencies:');
try {
final scope = CherryPick.openRootScope();
scope.enableCycleDetection(); // Включаем для безопасности
scope.installModules([
ImprovedUserModule(),
ImprovedOrderModule(),
]);
final userService = scope.resolve<ImprovedUserService>();
final orderService = scope.resolve<ImprovedOrderService>();
print('✅ Services created successfully');
// Демонстрация работы
userService.createUser('John');
orderService.createOrder('ORD-001', 'John');
final orders = orderService.getOrdersForUser('John');
print('✅ Orders for user John: $orders');
} catch (e) {
print('❌ Error: $e');
}
print('\n=== Recommendations ===');
print('1. Always enable circular dependency detection in development mode.');
print('2. Use repositories and services to separate concerns.');

View File

@@ -77,7 +77,8 @@ class CycleDetector {
);
if (_resolutionStack.contains(dependencyKey)) {
final cycleStartIndex = _resolutionHistory.indexOf(dependencyKey);
final cycle = _resolutionHistory.sublist(cycleStartIndex)..add(dependencyKey);
final cycle = _resolutionHistory.sublist(cycleStartIndex)
..add(dependencyKey);
_observer.onCycleDetected(cycle);
_observer.onError('Cycle detected for $dependencyKey', null, null);
throw CircularDependencyException(
@@ -99,7 +100,8 @@ class CycleDetector {
);
_resolutionStack.remove(dependencyKey);
// Only remove from history if it's the last one
if (_resolutionHistory.isNotEmpty && _resolutionHistory.last == dependencyKey) {
if (_resolutionHistory.isNotEmpty &&
_resolutionHistory.last == dependencyKey) {
_resolutionHistory.removeLast();
}
}
@@ -124,7 +126,8 @@ class CycleDetector {
}
/// Gets the current dependency resolution chain (for diagnostics or debugging).
List<String> get currentResolutionChain => List.unmodifiable(_resolutionHistory);
List<String> get currentResolutionChain =>
List.unmodifiable(_resolutionHistory);
/// Returns a unique string key for type [T] (+name).
String _createDependencyKey<T>(String? named) {
@@ -200,12 +203,13 @@ mixin CycleDetectionMixin {
return action();
}
final dependencyKey = named != null
? '${dependencyType.toString()}@$named'
final dependencyKey = named != null
? '${dependencyType.toString()}@$named'
: dependencyType.toString();
if (_cycleDetector!._resolutionStack.contains(dependencyKey)) {
final cycleStartIndex = _cycleDetector!._resolutionHistory.indexOf(dependencyKey);
final cycleStartIndex =
_cycleDetector!._resolutionHistory.indexOf(dependencyKey);
final cycle = _cycleDetector!._resolutionHistory.sublist(cycleStartIndex)
..add(dependencyKey);
observer.onCycleDetected(cycle);
@@ -223,7 +227,7 @@ mixin CycleDetectionMixin {
return action();
} finally {
_cycleDetector!._resolutionStack.remove(dependencyKey);
if (_cycleDetector!._resolutionHistory.isNotEmpty &&
if (_cycleDetector!._resolutionHistory.isNotEmpty &&
_cycleDetector!._resolutionHistory.last == dependencyKey) {
_cycleDetector!._resolutionHistory.removeLast();
}
@@ -231,6 +235,6 @@ mixin CycleDetectionMixin {
}
/// Gets the current active dependency resolution chain.
List<String> get currentResolutionChain =>
List<String> get currentResolutionChain =>
_cycleDetector?.currentResolutionChain ?? [];
}

View File

@@ -14,7 +14,6 @@
import 'dart:collection';
import 'package:cherrypick/cherrypick.dart';
/// GlobalCycleDetector detects and prevents circular dependencies across an entire DI scope hierarchy.
///
/// This is particularly important for modular/feature-based applications
@@ -45,13 +44,16 @@ class GlobalCycleDetector {
final List<String> _globalResolutionHistory = [];
// Map of active detectors for subscopes (rarely used directly)
final Map<String, CycleDetector> _scopeDetectors = HashMap<String, CycleDetector>();
final Map<String, CycleDetector> _scopeDetectors =
HashMap<String, CycleDetector>();
GlobalCycleDetector._internal({required CherryPickObserver observer}): _observer = observer;
GlobalCycleDetector._internal({required CherryPickObserver observer})
: _observer = observer;
/// 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!;
}
@@ -70,9 +72,11 @@ class GlobalCycleDetector {
if (_globalResolutionStack.contains(dependencyKey)) {
final cycleStartIndex = _globalResolutionHistory.indexOf(dependencyKey);
final cycle = _globalResolutionHistory.sublist(cycleStartIndex)..add(dependencyKey);
final cycle = _globalResolutionHistory.sublist(cycleStartIndex)
..add(dependencyKey);
_observer.onCycleDetected(cycle, scopeName: scopeId);
_observer.onError('Global circular dependency detected for $dependencyKey', null, null);
_observer.onError(
'Global circular dependency detected for $dependencyKey', null, null);
throw CircularDependencyException(
'Global circular dependency detected for $dependencyKey',
cycle,
@@ -88,7 +92,8 @@ class GlobalCycleDetector {
final dependencyKey = _createDependencyKeyFromType(T, named, scopeId);
_globalResolutionStack.remove(dependencyKey);
if (_globalResolutionHistory.isNotEmpty && _globalResolutionHistory.last == dependencyKey) {
if (_globalResolutionHistory.isNotEmpty &&
_globalResolutionHistory.last == dependencyKey) {
_globalResolutionHistory.removeLast();
}
}
@@ -101,13 +106,16 @@ class GlobalCycleDetector {
String? scopeId,
T Function() action,
) {
final dependencyKey = _createDependencyKeyFromType(dependencyType, named, scopeId);
final dependencyKey =
_createDependencyKeyFromType(dependencyType, named, scopeId);
if (_globalResolutionStack.contains(dependencyKey)) {
final cycleStartIndex = _globalResolutionHistory.indexOf(dependencyKey);
final cycle = _globalResolutionHistory.sublist(cycleStartIndex)..add(dependencyKey);
final cycle = _globalResolutionHistory.sublist(cycleStartIndex)
..add(dependencyKey);
_observer.onCycleDetected(cycle, scopeName: scopeId);
_observer.onError('Global circular dependency detected for $dependencyKey', null, null);
_observer.onError(
'Global circular dependency detected for $dependencyKey', null, null);
throw CircularDependencyException(
'Global circular dependency detected for $dependencyKey',
cycle,
@@ -121,7 +129,8 @@ class GlobalCycleDetector {
return action();
} finally {
_globalResolutionStack.remove(dependencyKey);
if (_globalResolutionHistory.isNotEmpty && _globalResolutionHistory.last == dependencyKey) {
if (_globalResolutionHistory.isNotEmpty &&
_globalResolutionHistory.last == dependencyKey) {
_globalResolutionHistory.removeLast();
}
}
@@ -129,7 +138,8 @@ class GlobalCycleDetector {
/// Get per-scope detector (not usually needed by consumers).
CycleDetector getScopeDetector(String scopeId) {
return _scopeDetectors.putIfAbsent(scopeId, () => CycleDetector(observer: CherryPick.globalObserver));
return _scopeDetectors.putIfAbsent(
scopeId, () => CycleDetector(observer: CherryPick.globalObserver));
}
/// Remove detector for a given scope.
@@ -144,7 +154,8 @@ class GlobalCycleDetector {
}
/// Get current global dependency resolution chain (for debugging or diagnostics).
List<String> get globalResolutionChain => List.unmodifiable(_globalResolutionHistory);
List<String> get globalResolutionChain =>
List.unmodifiable(_globalResolutionHistory);
/// Clears all global and per-scope state in this detector.
void clear() {
@@ -157,7 +168,8 @@ class GlobalCycleDetector {
void _detectorClear(detector) => detector.clear();
/// Creates a unique dependency key string including scope and name (for diagnostics/cycle checks).
String _createDependencyKeyFromType(Type type, String? named, String? scopeId) {
String _createDependencyKeyFromType(
Type type, String? named, String? scopeId) {
final typeName = type.toString();
final namePrefix = named != null ? '@$named' : '';
final scopePrefix = scopeId != null ? '[$scopeId]' : '';

View File

@@ -16,7 +16,6 @@ import 'package:cherrypick/src/global_cycle_detector.dart';
import 'package:cherrypick/src/observer.dart';
import 'package:meta/meta.dart';
Scope? _rootScope;
/// Global logger for all [Scope]s managed by [CherryPick].
@@ -80,7 +79,8 @@ class CherryPick {
if (_globalCycleDetectionEnabled && !_rootScope!.isCycleDetectionEnabled) {
_rootScope!.enableCycleDetection();
}
if (_globalCrossScopeCycleDetectionEnabled && !_rootScope!.isGlobalCycleDetectionEnabled) {
if (_globalCrossScopeCycleDetectionEnabled &&
!_rootScope!.isGlobalCycleDetectionEnabled) {
_rootScope!.enableGlobalCycleDetection();
}
return _rootScope!;
@@ -96,7 +96,8 @@ class CherryPick {
/// ```
static Future<void> closeRootScope() async {
if (_rootScope != null) {
await _rootScope!.dispose(); // Автоматический вызов dispose для rootScope!
await _rootScope!
.dispose(); // Автоматический вызов dispose для rootScope!
_rootScope = null;
}
}
@@ -141,13 +142,15 @@ class CherryPick {
/// ```dart
/// CherryPick.enableCycleDetectionForScope(scopeName: 'api.feature');
/// ```
static void enableCycleDetectionForScope({String scopeName = '', String separator = '.'}) {
static void enableCycleDetectionForScope(
{String scopeName = '', String separator = '.'}) {
final scope = _getScope(scopeName, separator);
scope.enableCycleDetection();
}
/// Disables cycle detection for a given scope. See [enableCycleDetectionForScope].
static void disableCycleDetectionForScope({String scopeName = '', String separator = '.'}) {
static void disableCycleDetectionForScope(
{String scopeName = '', String separator = '.'}) {
final scope = _getScope(scopeName, separator);
scope.disableCycleDetection();
}
@@ -158,7 +161,8 @@ class CherryPick {
/// ```dart
/// CherryPick.isCycleDetectionEnabledForScope(scopeName: 'feature.api');
/// ```
static bool isCycleDetectionEnabledForScope({String scopeName = '', String separator = '.'}) {
static bool isCycleDetectionEnabledForScope(
{String scopeName = '', String separator = '.'}) {
final scope = _getScope(scopeName, separator);
return scope.isCycleDetectionEnabled;
}
@@ -171,7 +175,8 @@ class CherryPick {
/// ```dart
/// print(CherryPick.getCurrentResolutionChain(scopeName: 'feature.api'));
/// ```
static List<String> getCurrentResolutionChain({String scopeName = '', String separator = '.'}) {
static List<String> getCurrentResolutionChain(
{String scopeName = '', String separator = '.'}) {
final scope = _getScope(scopeName, separator);
return scope.currentResolutionChain;
}
@@ -229,14 +234,13 @@ class CherryPick {
if (nameParts.isEmpty) {
throw Exception('Can not open sub scope because scopeName can not split');
}
final scope = nameParts.fold(
openRootScope(),
(Scope previous, String element) => previous.openSubScope(element)
);
final scope = nameParts.fold(openRootScope(),
(Scope previous, String element) => previous.openSubScope(element));
if (_globalCycleDetectionEnabled && !scope.isCycleDetectionEnabled) {
scope.enableCycleDetection();
}
if (_globalCrossScopeCycleDetectionEnabled && !scope.isGlobalCycleDetectionEnabled) {
if (_globalCrossScopeCycleDetectionEnabled &&
!scope.isGlobalCycleDetectionEnabled) {
scope.enableGlobalCycleDetection();
}
return scope;
@@ -252,21 +256,21 @@ class CherryPick {
/// CherryPick.closeScope(scopeName: 'network.super.api');
/// ```
@experimental
static Future<void> closeScope({String scopeName = '', String separator = '.'}) async {
static Future<void> closeScope(
{String scopeName = '', String separator = '.'}) async {
if (scopeName.isEmpty) {
await closeRootScope();
return;
}
final nameParts = scopeName.split(separator);
if (nameParts.isEmpty) {
throw Exception('Can not close sub scope because scopeName can not split');
throw Exception(
'Can not close sub scope because scopeName can not split');
}
if (nameParts.length > 1) {
final lastPart = nameParts.removeLast();
final scope = nameParts.fold(
openRootScope(),
(Scope previous, String element) => previous.openSubScope(element)
);
final scope = nameParts.fold(openRootScope(),
(Scope previous, String element) => previous.openSubScope(element));
await scope.closeSubScope(lastPart);
} else {
await openRootScope().closeSubScope(nameParts.first);
@@ -316,7 +320,8 @@ class CherryPick {
/// print('Global cross-scope detection is ON');
/// }
/// ```
static bool get isGlobalCrossScopeCycleDetectionEnabled => _globalCrossScopeCycleDetectionEnabled;
static bool get isGlobalCrossScopeCycleDetectionEnabled =>
_globalCrossScopeCycleDetectionEnabled;
/// Returns the current global dependency resolution chain (across all scopes).
///
@@ -367,10 +372,11 @@ class CherryPick {
/// ```dart
/// final featureScope = CherryPick.openGlobalSafeScope(scopeName: 'featureA.api');
/// ```
static Scope openGlobalSafeScope({String scopeName = '', String separator = '.'}) {
static Scope openGlobalSafeScope(
{String scopeName = '', String separator = '.'}) {
final scope = openScope(scopeName: scopeName, separator: separator);
scope.enableCycleDetection();
scope.enableGlobalCycleDetection();
return scope;
}
}
}

View File

@@ -16,13 +16,13 @@ import 'package:cherrypick/src/binding.dart';
import 'package:cherrypick/src/scope.dart';
/// 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 {
@@ -33,12 +33,12 @@ import 'package:cherrypick/src/scope.dart';
/// bind<Config>().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 {

View File

@@ -49,7 +49,8 @@ abstract class CherryPickObserver {
/// ```dart
/// observer.onInstanceCreated('MyService', MyService, instance, scopeName: 'root');
/// ```
void onInstanceCreated(String name, Type type, Object instance, {String? scopeName});
void onInstanceCreated(String name, Type type, Object instance,
{String? scopeName});
/// Called when an instance is disposed (removed from cache and/or finalized).
///
@@ -57,7 +58,8 @@ abstract class CherryPickObserver {
/// ```dart
/// observer.onInstanceDisposed('MyService', MyService, instance, scopeName: 'root');
/// ```
void onInstanceDisposed(String name, Type type, Object instance, {String? scopeName});
void onInstanceDisposed(String name, Type type, Object instance,
{String? scopeName});
// === Module events ===
/// Called when modules are installed into the container.
@@ -157,19 +159,23 @@ class PrintCherryPickObserver implements CherryPickObserver {
print('[request][CherryPick] $name$type (scope: $scopeName)');
@override
void onInstanceCreated(String name, Type type, Object instance, {String? scopeName}) =>
print('[create][CherryPick] $name$type => $instance (scope: $scopeName)');
void onInstanceCreated(String name, Type type, Object instance,
{String? scopeName}) =>
print(
'[create][CherryPick] $name$type => $instance (scope: $scopeName)');
@override
void onInstanceDisposed(String name, Type type, Object instance, {String? scopeName}) =>
print('[dispose][CherryPick] $name$type => $instance (scope: $scopeName)');
void onInstanceDisposed(String name, Type type, Object instance,
{String? scopeName}) =>
print(
'[dispose][CherryPick] $name$type => $instance (scope: $scopeName)');
@override
void onModulesInstalled(List<String> modules, {String? scopeName}) =>
print('[modules installed][CherryPick] ${modules.join(', ')} (scope: $scopeName)');
void onModulesInstalled(List<String> modules, {String? scopeName}) => print(
'[modules installed][CherryPick] ${modules.join(', ')} (scope: $scopeName)');
@override
void onModulesRemoved(List<String> modules, {String? scopeName}) =>
print('[modules removed][CherryPick] ${modules.join(', ')} (scope: $scopeName)');
void onModulesRemoved(List<String> modules, {String? scopeName}) => print(
'[modules removed][CherryPick] ${modules.join(', ')} (scope: $scopeName)');
@override
void onScopeOpened(String name) => print('[scope opened][CherryPick] $name');
@@ -178,8 +184,8 @@ class PrintCherryPickObserver implements CherryPickObserver {
void onScopeClosed(String name) => print('[scope closed][CherryPick] $name');
@override
void onCycleDetected(List<String> chain, {String? scopeName}) =>
print('[cycle][CherryPick] Detected: ${chain.join(' -> ')}${scopeName != null ? ' (scope: $scopeName)' : ''}');
void onCycleDetected(List<String> chain, {String? scopeName}) => print(
'[cycle][CherryPick] Detected: ${chain.join(' -> ')}${scopeName != null ? ' (scope: $scopeName)' : ''}');
@override
void onCacheHit(String name, Type type, {String? scopeName}) =>
@@ -210,9 +216,11 @@ class SilentCherryPickObserver implements CherryPickObserver {
@override
void onInstanceRequested(String name, Type type, {String? scopeName}) {}
@override
void onInstanceCreated(String name, Type type, Object instance, {String? scopeName}) {}
void onInstanceCreated(String name, Type type, Object instance,
{String? scopeName}) {}
@override
void onInstanceDisposed(String name, Type type, Object instance, {String? scopeName}) {}
void onInstanceDisposed(String name, Type type, Object instance,
{String? scopeName}) {}
@override
void onModulesInstalled(List<String> modules, {String? scopeName}) {}
@override

View File

@@ -68,7 +68,8 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
final Map<String, Scope> _scopeMap = HashMap();
Scope(this._parentScope, {required CherryPickObserver observer}) : _observer = observer {
Scope(this._parentScope, {required CherryPickObserver observer})
: _observer = observer {
setScopeId(_generateScopeId());
observer.onScopeOpened(scopeId ?? 'NO_ID');
observer.onDiagnostic(
@@ -87,7 +88,6 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
// индекс для мгновенного поиска bindingов
final Map<Object, Map<String?, BindingResolver>> _bindingResolvers = {};
/// Generates a unique identifier string for this scope instance.
///
/// Used internally for diagnostics, logging and global scope tracking.
@@ -280,7 +280,8 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
return withCycleDetection<T>(T, named, () {
var resolved = _tryResolveInternal<T>(named: named, params: params);
if (resolved != null) {
observer.onInstanceCreated(T.toString(), T, resolved, scopeName: scopeId);
observer.onInstanceCreated(T.toString(), T, resolved,
scopeName: scopeId);
observer.onDiagnostic(
'Successfully resolved: $T',
details: {
@@ -360,10 +361,12 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
T result;
if (isGlobalCycleDetectionEnabled) {
result = await withGlobalCycleDetection<Future<T>>(T, named, () async {
return await _resolveAsyncWithLocalDetection<T>(named: named, params: params);
return await _resolveAsyncWithLocalDetection<T>(
named: named, params: params);
});
} else {
result = await _resolveAsyncWithLocalDetection<T>(named: named, params: params);
result = await _resolveAsyncWithLocalDetection<T>(
named: named, params: params);
}
_trackDisposable(result);
return result;
@@ -371,11 +374,14 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
/// Resolves [T] asynchronously using local cycle detector. Throws if not found.
/// Internal implementation for async [resolveAsync].
Future<T> _resolveAsyncWithLocalDetection<T>({String? named, dynamic params}) async {
Future<T> _resolveAsyncWithLocalDetection<T>(
{String? named, dynamic params}) async {
return withCycleDetection<Future<T>>(T, named, () async {
var resolved = await _tryResolveAsyncInternal<T>(named: named, params: params);
var resolved =
await _tryResolveAsyncInternal<T>(named: named, params: params);
if (resolved != null) {
observer.onInstanceCreated(T.toString(), T, resolved, scopeName: scopeId);
observer.onInstanceCreated(T.toString(), T, resolved,
scopeName: scopeId);
observer.onDiagnostic(
'Successfully async resolved: $T',
details: {
@@ -410,10 +416,12 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
T? result;
if (isGlobalCycleDetectionEnabled) {
result = await withGlobalCycleDetection<Future<T?>>(T, named, () async {
return await _tryResolveAsyncWithLocalDetection<T>(named: named, params: params);
return await _tryResolveAsyncWithLocalDetection<T>(
named: named, params: params);
});
} else {
result = await _tryResolveAsyncWithLocalDetection<T>(named: named, params: params);
result = await _tryResolveAsyncWithLocalDetection<T>(
named: named, params: params);
}
if (result != null) _trackDisposable(result);
return result;
@@ -421,7 +429,8 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
/// Attempts to resolve [T] asynchronously using local cycle detector. Returns null if missing.
/// Internal implementation for async [tryResolveAsync].
Future<T?> _tryResolveAsyncWithLocalDetection<T>({String? named, dynamic params}) async {
Future<T?> _tryResolveAsyncWithLocalDetection<T>(
{String? named, dynamic params}) async {
if (isCycleDetectionEnabled) {
return withCycleDetection<Future<T?>>(T, named, () async {
return await _tryResolveAsyncInternal<T>(named: named, params: params);
@@ -432,7 +441,8 @@ 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}) async {
final resolver = _findBindingResolver<T>(named);
// 1 - Try from own modules; 2 - Fallback to parent
return resolver?.resolveAsync(params) ??

View File

@@ -12,6 +12,7 @@ class DummyModule extends Module {
}
class A {}
class B {}
class CyclicModule extends Module {
@@ -52,10 +53,13 @@ void main() {
throwsA(isA<CircularDependencyException>()),
);
// Проверяем, что цикл зафиксирован либо в errors, либо в diagnostics либо cycles
final foundInErrors = observer.errors.any((m) => m.contains('cycle detected'));
final foundInDiagnostics = observer.diagnostics.any((m) => m.contains('cycle detected'));
final foundInErrors =
observer.errors.any((m) => m.contains('cycle detected'));
final foundInDiagnostics =
observer.diagnostics.any((m) => m.contains('cycle detected'));
final foundCycleNotified = observer.cycles.isNotEmpty;
expect(foundInErrors || foundInDiagnostics || foundCycleNotified, isTrue,
reason: 'Ожидаем хотя бы один лог о цикле! errors: ${observer.errors}\ndiag: ${observer.diagnostics}\ncycles: ${observer.cycles}');
reason:
'Ожидаем хотя бы один лог о цикле! errors: ${observer.errors}\ndiag: ${observer.diagnostics}\ncycles: ${observer.cycles}');
});
}
}

View File

@@ -15,9 +15,8 @@ class MockObserver implements CherryPickObserver {
void onWarning(String message, {Object? details}) => warnings.add(message);
@override
void onError(String message, Object? error, StackTrace? stackTrace) =>
errors.add(
'$message${error != null ? ' $error' : ''}${stackTrace != null ? '\n$stackTrace' : ''}');
void onError(String message, Object? error, StackTrace? stackTrace) => errors.add(
'$message${error != null ? ' $error' : ''}${stackTrace != null ? '\n$stackTrace' : ''}');
@override
void onCycleDetected(List<String> chain, {String? scopeName}) =>
@@ -30,9 +29,11 @@ class MockObserver implements CherryPickObserver {
@override
void onInstanceRequested(String name, Type type, {String? scopeName}) {}
@override
void onInstanceCreated(String name, Type type, Object instance, {String? scopeName}) {}
void onInstanceCreated(String name, Type type, Object instance,
{String? scopeName}) {}
@override
void onInstanceDisposed(String name, Type type, Object instance, {String? scopeName}) {}
void onInstanceDisposed(String name, Type type, Object instance,
{String? scopeName}) {}
@override
void onModulesInstalled(List<String> moduleNames, {String? scopeName}) {}
@override

View File

@@ -30,7 +30,7 @@ void main() {
final rootScope = CherryPick.openSafeRootScope();
final level1Scope = rootScope.openSubScope('level1');
final level2Scope = level1Scope.openSubScope('level2');
level1Scope.enableCycleDetection();
level2Scope.enableCycleDetection();
@@ -46,14 +46,16 @@ void main() {
);
});
test('current implementation limitation - may not detect cross-scope cycles', () {
test(
'current implementation limitation - may not detect cross-scope cycles',
() {
// Этот тест демонстрирует ограничение текущей реализации
final parentScope = CherryPick.openRootScope();
parentScope.enableCycleDetection();
final childScope = parentScope.openSubScope('child');
// НЕ включаем cycle detection для дочернего скоупа
parentScope.installModules([ParentScopeModule()]);
childScope.installModules([ChildScopeModule()]);

View File

@@ -18,7 +18,7 @@ void main() {
test('should detect simple circular dependency', () {
detector.startResolving<String>();
expect(
() => detector.startResolving<String>(),
throwsA(isA<CircularDependencyException>()),
@@ -27,7 +27,7 @@ void main() {
test('should detect circular dependency with named bindings', () {
detector.startResolving<String>(named: 'test');
expect(
() => detector.startResolving<String>(named: 'test'),
throwsA(isA<CircularDependencyException>()),
@@ -37,7 +37,7 @@ void main() {
test('should allow different types to be resolved simultaneously', () {
detector.startResolving<String>();
detector.startResolving<int>();
expect(() => detector.finishResolving<int>(), returnsNormally);
expect(() => detector.finishResolving<String>(), returnsNormally);
});
@@ -46,32 +46,31 @@ void main() {
detector.startResolving<String>();
detector.startResolving<int>();
detector.startResolving<bool>();
expect(
() => detector.startResolving<String>(),
throwsA(predicate((e) =>
e is CircularDependencyException &&
e.dependencyChain.contains('String') &&
e.dependencyChain.length > 1
)),
throwsA(predicate((e) =>
e is CircularDependencyException &&
e.dependencyChain.contains('String') &&
e.dependencyChain.length > 1)),
);
});
test('should clear state properly', () {
detector.startResolving<String>();
detector.clear();
expect(() => detector.startResolving<String>(), returnsNormally);
});
test('should track resolution history correctly', () {
detector.startResolving<String>();
detector.startResolving<int>();
expect(detector.currentResolutionChain, contains('String'));
expect(detector.currentResolutionChain, contains('int'));
expect(detector.currentResolutionChain.length, equals(2));
detector.finishResolving<int>();
expect(detector.currentResolutionChain.length, equals(1));
expect(detector.currentResolutionChain, contains('String'));
@@ -82,7 +81,7 @@ void main() {
test('should detect circular dependency in real scenario', () {
final scope = CherryPick.openRootScope();
scope.enableCycleDetection();
// Создаем циклическую зависимость: A зависит от B, B зависит от A
scope.installModules([
CircularModuleA(),
@@ -98,7 +97,7 @@ void main() {
test('should work normally without cycle detection enabled', () {
final scope = CherryPick.openRootScope();
// Не включаем обнаружение циклических зависимостей
scope.installModules([
SimpleModule(),
]);
@@ -111,7 +110,7 @@ void main() {
final scope = CherryPick.openRootScope();
scope.enableCycleDetection();
expect(scope.isCycleDetectionEnabled, isTrue);
scope.disableCycleDetection();
expect(scope.isCycleDetectionEnabled, isFalse);
});
@@ -119,7 +118,7 @@ void main() {
test('should handle named dependencies in cycle detection', () {
final scope = CherryPick.openRootScope();
scope.enableCycleDetection();
scope.installModules([
NamedCircularModule(),
]);
@@ -133,7 +132,7 @@ void main() {
test('should detect cycles in async resolution', () async {
final scope = CherryPick.openRootScope();
scope.enableCycleDetection();
scope.installModules([
AsyncCircularModule(),
]);
@@ -161,14 +160,16 @@ class ServiceB {
class CircularModuleA extends Module {
@override
void builder(Scope currentScope) {
bind<ServiceA>().toProvide(() => ServiceA(currentScope.resolve<ServiceB>()));
bind<ServiceA>()
.toProvide(() => ServiceA(currentScope.resolve<ServiceB>()));
}
}
class CircularModuleB extends Module {
@override
void builder(Scope currentScope) {
bind<ServiceB>().toProvide(() => ServiceB(currentScope.resolve<ServiceA>()));
bind<ServiceB>()
.toProvide(() => ServiceB(currentScope.resolve<ServiceA>()));
}
}
@@ -210,7 +211,7 @@ class AsyncCircularModule extends Module {
final serviceB = await currentScope.resolveAsync<AsyncServiceB>();
return AsyncServiceA(serviceB);
});
// ignore: deprecated_member_use_from_same_package
bind<AsyncServiceB>().toProvideAsync(() async {
final serviceA = await currentScope.resolveAsync<AsyncServiceA>();

View File

@@ -22,50 +22,57 @@ void main() {
group('Global Cross-Scope Cycle Detection', () {
test('should enable global cross-scope cycle detection', () {
expect(CherryPick.isGlobalCrossScopeCycleDetectionEnabled, isFalse);
CherryPick.enableGlobalCrossScopeCycleDetection();
expect(CherryPick.isGlobalCrossScopeCycleDetectionEnabled, isTrue);
});
test('should disable global cross-scope cycle detection', () {
CherryPick.enableGlobalCrossScopeCycleDetection();
expect(CherryPick.isGlobalCrossScopeCycleDetectionEnabled, isTrue);
CherryPick.disableGlobalCrossScopeCycleDetection();
expect(CherryPick.isGlobalCrossScopeCycleDetectionEnabled, isFalse);
});
test('should automatically enable global cycle detection for new root scope', () {
test(
'should automatically enable global cycle detection for new root scope',
() {
CherryPick.enableGlobalCrossScopeCycleDetection();
final scope = CherryPick.openRootScope();
expect(scope.isGlobalCycleDetectionEnabled, isTrue);
});
test('should automatically enable global cycle detection for existing root scope', () {
test(
'should automatically enable global cycle detection for existing root scope',
() {
final scope = CherryPick.openRootScope();
expect(scope.isGlobalCycleDetectionEnabled, isFalse);
CherryPick.enableGlobalCrossScopeCycleDetection();
expect(scope.isGlobalCycleDetectionEnabled, isTrue);
});
});
group('Global Safe Scope Creation', () {
test('should create global safe root scope with both detections enabled', () {
test('should create global safe root scope with both detections enabled',
() {
final scope = CherryPick.openGlobalSafeRootScope();
expect(scope.isCycleDetectionEnabled, isTrue);
expect(scope.isGlobalCycleDetectionEnabled, isTrue);
});
test('should create global safe sub-scope with both detections enabled', () {
final scope = CherryPick.openGlobalSafeScope(scopeName: 'feature.global');
test('should create global safe sub-scope with both detections enabled',
() {
final scope =
CherryPick.openGlobalSafeScope(scopeName: 'feature.global');
expect(scope.isCycleDetectionEnabled, isTrue);
expect(scope.isGlobalCycleDetectionEnabled, isTrue);
});
@@ -104,7 +111,7 @@ void main() {
test('should provide detailed global resolution chain in exception', () {
final scope = CherryPick.openGlobalSafeRootScope();
scope.installModules([GlobalParentModule()]);
final childScope = scope.openSubScope('child');
childScope.installModules([GlobalChildModule()]);
@@ -114,11 +121,11 @@ void main() {
} catch (e) {
expect(e, isA<CircularDependencyException>());
final circularError = e as CircularDependencyException;
// Проверяем, что цепочка содержит информацию о скоупах
expect(circularError.dependencyChain, isNotEmpty);
expect(circularError.dependencyChain.length, greaterThan(1));
// Цепочка должна содержать оба сервиса
final chainString = circularError.dependencyChain.join(' -> ');
expect(chainString, contains('GlobalServiceA'));
@@ -144,11 +151,11 @@ void main() {
CherryPick.enableGlobalCrossScopeCycleDetection();
// ignore: unused_local_variable
final scope = CherryPick.openGlobalSafeRootScope();
expect(CherryPick.getGlobalResolutionChain(), isEmpty);
CherryPick.clearGlobalCycleDetector();
// После очистки детектор должен быть сброшен
expect(CherryPick.getGlobalResolutionChain(), isEmpty);
});
@@ -157,10 +164,10 @@ void main() {
group('Inheritance of Global Settings', () {
test('should inherit global cycle detection in child scopes', () {
CherryPick.enableGlobalCrossScopeCycleDetection();
final parentScope = CherryPick.openRootScope();
final childScope = parentScope.openSubScope('child');
expect(parentScope.isGlobalCycleDetectionEnabled, isTrue);
expect(childScope.isGlobalCycleDetectionEnabled, isTrue);
});
@@ -168,9 +175,9 @@ void main() {
test('should inherit both local and global cycle detection', () {
CherryPick.enableGlobalCycleDetection();
CherryPick.enableGlobalCrossScopeCycleDetection();
final scope = CherryPick.openScope(scopeName: 'feature.test');
expect(scope.isCycleDetectionEnabled, isTrue);
expect(scope.isGlobalCycleDetectionEnabled, isTrue);
});

View File

@@ -24,53 +24,59 @@ void main() {
group('Global Cycle Detection', () {
test('should enable global cycle detection', () {
expect(CherryPick.isGlobalCycleDetectionEnabled, isFalse);
CherryPick.enableGlobalCycleDetection();
expect(CherryPick.isGlobalCycleDetectionEnabled, isTrue);
});
test('should disable global cycle detection', () {
CherryPick.enableGlobalCycleDetection();
expect(CherryPick.isGlobalCycleDetectionEnabled, isTrue);
CherryPick.disableGlobalCycleDetection();
expect(CherryPick.isGlobalCycleDetectionEnabled, isFalse);
});
test('should automatically enable cycle detection for new root scope when global is enabled', () {
test(
'should automatically enable cycle detection for new root scope when global is enabled',
() {
CherryPick.enableGlobalCycleDetection();
final scope = CherryPick.openRootScope();
expect(scope.isCycleDetectionEnabled, isTrue);
});
test('should automatically enable cycle detection for existing root scope when global is enabled', () {
test(
'should automatically enable cycle detection for existing root scope when global is enabled',
() {
final scope = CherryPick.openRootScope();
expect(scope.isCycleDetectionEnabled, isFalse);
CherryPick.enableGlobalCycleDetection();
expect(scope.isCycleDetectionEnabled, isTrue);
});
test('should automatically disable cycle detection for existing root scope when global is disabled', () {
test(
'should automatically disable cycle detection for existing root scope when global is disabled',
() {
CherryPick.enableGlobalCycleDetection();
final scope = CherryPick.openRootScope();
expect(scope.isCycleDetectionEnabled, isTrue);
CherryPick.disableGlobalCycleDetection();
expect(scope.isCycleDetectionEnabled, isFalse);
});
test('should apply global setting to sub-scopes', () {
CherryPick.enableGlobalCycleDetection();
final scope = CherryPick.openScope(scopeName: 'test.subscope');
expect(scope.isCycleDetectionEnabled, isTrue);
});
});
@@ -79,9 +85,9 @@ void main() {
test('should enable cycle detection for root scope', () {
final scope = CherryPick.openRootScope();
expect(scope.isCycleDetectionEnabled, isFalse);
CherryPick.enableCycleDetectionForScope();
expect(CherryPick.isCycleDetectionEnabledForScope(), isTrue);
expect(scope.isCycleDetectionEnabled, isTrue);
});
@@ -89,91 +95,103 @@ void main() {
test('should disable cycle detection for root scope', () {
CherryPick.enableCycleDetectionForScope();
expect(CherryPick.isCycleDetectionEnabledForScope(), isTrue);
CherryPick.disableCycleDetectionForScope();
expect(CherryPick.isCycleDetectionEnabledForScope(), isFalse);
});
test('should enable cycle detection for specific scope', () {
final scopeName = 'feature.auth';
CherryPick.openScope(scopeName: scopeName);
expect(CherryPick.isCycleDetectionEnabledForScope(scopeName: scopeName), isFalse);
expect(CherryPick.isCycleDetectionEnabledForScope(scopeName: scopeName),
isFalse);
CherryPick.enableCycleDetectionForScope(scopeName: scopeName);
expect(CherryPick.isCycleDetectionEnabledForScope(scopeName: scopeName), isTrue);
expect(CherryPick.isCycleDetectionEnabledForScope(scopeName: scopeName),
isTrue);
});
test('should disable cycle detection for specific scope', () {
final scopeName = 'feature.auth';
CherryPick.enableCycleDetectionForScope(scopeName: scopeName);
expect(CherryPick.isCycleDetectionEnabledForScope(scopeName: scopeName), isTrue);
expect(CherryPick.isCycleDetectionEnabledForScope(scopeName: scopeName),
isTrue);
CherryPick.disableCycleDetectionForScope(scopeName: scopeName);
expect(CherryPick.isCycleDetectionEnabledForScope(scopeName: scopeName), isFalse);
expect(CherryPick.isCycleDetectionEnabledForScope(scopeName: scopeName),
isFalse);
});
});
group('Safe Scope Creation', () {
test('should create safe root scope with cycle detection enabled', () {
final scope = CherryPick.openSafeRootScope();
expect(scope.isCycleDetectionEnabled, isTrue);
});
test('should create safe sub-scope with cycle detection enabled', () {
final scope = CherryPick.openSafeScope(scopeName: 'feature.safe');
expect(scope.isCycleDetectionEnabled, isTrue);
});
test('safe scope should work independently of global setting', () {
// Глобальная настройка отключена
expect(CherryPick.isGlobalCycleDetectionEnabled, isFalse);
final scope = CherryPick.openSafeScope(scopeName: 'feature.independent');
final scope =
CherryPick.openSafeScope(scopeName: 'feature.independent');
expect(scope.isCycleDetectionEnabled, isTrue);
});
});
group('Resolution Chain Tracking', () {
test('should return empty resolution chain for scope without cycle detection', () {
test(
'should return empty resolution chain for scope without cycle detection',
() {
CherryPick.openRootScope();
final chain = CherryPick.getCurrentResolutionChain();
expect(chain, isEmpty);
});
test('should return empty resolution chain for scope with cycle detection but no active resolution', () {
test(
'should return empty resolution chain for scope with cycle detection but no active resolution',
() {
CherryPick.enableCycleDetectionForScope();
final chain = CherryPick.getCurrentResolutionChain();
expect(chain, isEmpty);
});
test('should track resolution chain for specific scope', () {
final scopeName = 'feature.tracking';
CherryPick.enableCycleDetectionForScope(scopeName: scopeName);
final chain = CherryPick.getCurrentResolutionChain(scopeName: scopeName);
final chain =
CherryPick.getCurrentResolutionChain(scopeName: scopeName);
expect(chain, isEmpty); // Пустая, так как нет активного разрешения
});
});
group('Integration with Circular Dependencies', () {
test('should detect circular dependency with global cycle detection enabled', () {
test(
'should detect circular dependency with global cycle detection enabled',
() {
CherryPick.enableGlobalCycleDetection();
final scope = CherryPick.openRootScope();
scope.installModules([CircularTestModule()]);
expect(
() => scope.resolve<CircularServiceA>(),
throwsA(isA<CircularDependencyException>()),
@@ -183,44 +201,54 @@ void main() {
test('should detect circular dependency with safe scope', () {
final scope = CherryPick.openSafeRootScope();
scope.installModules([CircularTestModule()]);
expect(
() => scope.resolve<CircularServiceA>(),
throwsA(isA<CircularDependencyException>()),
);
});
test('should not detect circular dependency when cycle detection is disabled', () {
test(
'should not detect circular dependency when cycle detection is disabled',
() {
final scope = CherryPick.openRootScope();
scope.installModules([CircularTestModule()]);
// Без обнаружения циклических зависимостей не будет выброшено CircularDependencyException,
// но может произойти StackOverflowError при попытке создания объекта
expect(() => scope.resolve<CircularServiceA>(),
throwsA(isA<StackOverflowError>()));
expect(() => scope.resolve<CircularServiceA>(),
throwsA(isA<StackOverflowError>()));
});
});
group('Scope Name Handling', () {
test('should handle empty scope name as root scope', () {
CherryPick.enableCycleDetectionForScope(scopeName: '');
expect(CherryPick.isCycleDetectionEnabledForScope(scopeName: ''), isTrue);
expect(
CherryPick.isCycleDetectionEnabledForScope(scopeName: ''), isTrue);
expect(CherryPick.isCycleDetectionEnabledForScope(), isTrue);
});
test('should handle complex scope names', () {
final complexScopeName = 'app.feature.auth.login';
CherryPick.enableCycleDetectionForScope(scopeName: complexScopeName);
expect(CherryPick.isCycleDetectionEnabledForScope(scopeName: complexScopeName), isTrue);
expect(
CherryPick.isCycleDetectionEnabledForScope(
scopeName: complexScopeName),
isTrue);
});
test('should handle custom separator', () {
final scopeName = 'app/feature/auth';
CherryPick.enableCycleDetectionForScope(scopeName: scopeName, separator: '/');
expect(CherryPick.isCycleDetectionEnabledForScope(scopeName: scopeName, separator: '/'), isTrue);
CherryPick.enableCycleDetectionForScope(
scopeName: scopeName, separator: '/');
expect(
CherryPick.isCycleDetectionEnabledForScope(
scopeName: scopeName, separator: '/'),
isTrue);
});
});
});
@@ -240,7 +268,9 @@ class CircularServiceB {
class CircularTestModule extends Module {
@override
void builder(Scope currentScope) {
bind<CircularServiceA>().toProvide(() => CircularServiceA(currentScope.resolve<CircularServiceB>()));
bind<CircularServiceB>().toProvide(() => CircularServiceB(currentScope.resolve<CircularServiceA>()));
bind<CircularServiceA>().toProvide(
() => CircularServiceA(currentScope.resolve<CircularServiceB>()));
bind<CircularServiceB>().toProvide(
() => CircularServiceB(currentScope.resolve<CircularServiceA>()));
}
}

View File

@@ -1,4 +1,5 @@
import 'package:cherrypick/cherrypick.dart' show Disposable, Module, Scope, CherryPick;
import 'package:cherrypick/cherrypick.dart'
show Disposable, Module, Scope, CherryPick;
import 'dart:async';
import 'package:test/test.dart';
import '../mock_logger.dart';
@@ -18,7 +19,9 @@ class AsyncExampleDisposable implements Disposable {
class AsyncExampleModule extends Module {
@override
void builder(Scope scope) {
bind<AsyncExampleDisposable>().toProvide(() => AsyncExampleDisposable()).singleton();
bind<AsyncExampleDisposable>()
.toProvide(() => AsyncExampleDisposable())
.singleton();
}
}
@@ -49,7 +52,9 @@ class CountingDisposable implements Disposable {
class ModuleCountingDisposable extends Module {
@override
void builder(Scope scope) {
bind<CountingDisposable>().toProvide(() => CountingDisposable()).singleton();
bind<CountingDisposable>()
.toProvide(() => CountingDisposable())
.singleton();
}
}
@@ -97,10 +102,9 @@ class AsyncModule extends Module {
bind<AsyncCreatedDisposable>()
// ignore: deprecated_member_use_from_same_package
.toProvideAsync(() async {
await Future.delayed(Duration(milliseconds: 10));
return AsyncCreatedDisposable();
})
.singleton();
await Future.delayed(Duration(milliseconds: 10));
return AsyncCreatedDisposable();
}).singleton();
}
}
@@ -119,7 +123,8 @@ void main() {
final scope = Scope(null, observer: observer);
expect(Scope(scope, observer: observer), isNotNull); // эквивалент
});
test('closeSubScope removes subscope so next openSubScope returns new', () async {
test('closeSubScope removes subscope so next openSubScope returns new',
() async {
final observer = MockObserver();
final scope = Scope(null, observer: observer);
final subScope = scope.openSubScope("child");
@@ -181,7 +186,8 @@ void main() {
});
test("After dropModules resolves fail", () {
final observer = MockObserver();
final scope = Scope(null, observer: observer)..installModules([TestModule<int>(value: 5)]);
final scope = Scope(null, observer: observer)
..installModules([TestModule<int>(value: 5)]);
expect(scope.resolve<int>(), 5);
scope.dropModules();
expect(() => scope.resolve<int>(), throwsA(isA<StateError>()));
@@ -294,7 +300,8 @@ void main() {
await scope.dispose();
expect(t.disposed, isTrue);
});
test('scope.disposeAsync calls dispose on all unique disposables', () async {
test('scope.disposeAsync calls dispose on all unique disposables',
() async {
final scope = Scope(null, observer: MockObserver());
scope.installModules([ModuleWithDisposable()]);
final t1 = scope.resolve<TestDisposable>();
@@ -305,7 +312,8 @@ void main() {
expect(t1.disposed, isTrue);
expect(t2.disposed, isTrue);
});
test('calling disposeAsync twice does not throw and not call twice', () async {
test('calling disposeAsync twice does not throw and not call twice',
() async {
final scope = CherryPick.openRootScope();
scope.installModules([ModuleWithDisposable()]);
final t = scope.resolve<TestDisposable>();
@@ -313,7 +321,8 @@ void main() {
await scope.dispose();
expect(t.disposed, isTrue);
});
test('Non-disposable dependency is ignored by scope.disposeAsync', () async {
test('Non-disposable dependency is ignored by scope.disposeAsync',
() async {
final scope = CherryPick.openRootScope();
scope.installModules([ModuleWithDisposable()]);
final s = scope.resolve<String>();
@@ -327,7 +336,8 @@ void main() {
group('Scope/subScope dispose edge cases', () {
test('Dispose called in closed subScope only', () async {
final root = CherryPick.openRootScope();
final sub = root.openSubScope('feature')..installModules([ModuleCountingDisposable()]);
final sub = root.openSubScope('feature')
..installModules([ModuleCountingDisposable()]);
final d = sub.resolve<CountingDisposable>();
expect(d.disposeCount, 0);
@@ -339,7 +349,8 @@ void main() {
expect(d.disposeCount, 1);
// Повторное открытие subScope создает NEW instance (dispose на старый не вызовется снова)
final sub2 = root.openSubScope('feature')..installModules([ModuleCountingDisposable()]);
final sub2 = root.openSubScope('feature')
..installModules([ModuleCountingDisposable()]);
final d2 = sub2.resolve<CountingDisposable>();
expect(identical(d, d2), isFalse);
await root.closeSubScope('feature');
@@ -347,8 +358,14 @@ void main() {
});
test('Dispose for all nested subScopes on root disposeAsync', () async {
final root = CherryPick.openRootScope();
root.openSubScope('a').openSubScope('b').installModules([ModuleCountingDisposable()]);
final d = root.openSubScope('a').openSubScope('b').resolve<CountingDisposable>();
root
.openSubScope('a')
.openSubScope('b')
.installModules([ModuleCountingDisposable()]);
final d = root
.openSubScope('a')
.openSubScope('b')
.resolve<CountingDisposable>();
await root.dispose();
expect(d.disposeCount, 1);
});
@@ -357,11 +374,12 @@ void main() {
// --------------------------------------------------------------------------
group('Async disposable (Future test)', () {
test('Async Disposable is awaited on disposeAsync', () async {
final scope = CherryPick.openRootScope()..installModules([AsyncExampleModule()]);
final scope = CherryPick.openRootScope()
..installModules([AsyncExampleModule()]);
final d = scope.resolve<AsyncExampleDisposable>();
expect(d.disposed, false);
await scope.dispose();
expect(d.disposed, true);
});
});
}
}

View File

@@ -21,7 +21,7 @@ import 'package:flutter/widgets.dart';
/// Place `CherryPickProvider` at the top of your widget subtree to make a
/// [Scope] (or its descendants) available via `CherryPickProvider.of(context)`.
///
/// This is the recommended entry point for connecting CherryPick DI to your
/// This is the recommended entry point for connecting CherryPick DI to your
/// Flutter app or feature area, enabling context-based scope management and
/// DI resolution in child widgets.
///
@@ -36,7 +36,7 @@ import 'package:flutter/widgets.dart';
/// ),
/// );
/// }
///
///
/// // In any widget:
/// final provider = CherryPickProvider.of(context);
/// final scope = provider.openRootScope();

View File

@@ -248,7 +248,6 @@ class _ParsedInjectField {
/// Name qualifier for named resolution, or null if not set.
final String? namedValue;
_ParsedInjectField({
required this.fieldName,
required this.coreType,

View File

@@ -23,6 +23,7 @@ import 'annotation_validator.dart';
enum BindingType {
/// Direct instance returned from the method (@instance).
instance,
/// Provider/factory function (@provide).
provide;
}

View File

@@ -244,8 +244,7 @@ void main() {
final result = bindSpec.generateBind(4);
expect(
result,
equals(
" bind<ApiClient>()\n"
equals(" bind<ApiClient>()\n"
" .toProvideAsync(() => createApiClient())\n"
" .withName('mainApi')\n"
" .singleton();"));

View File

@@ -127,28 +127,28 @@ packages:
path: "../../cherrypick"
relative: true
source: path
version: "3.0.0-dev.8"
version: "3.0.0-dev.9"
cherrypick_annotations:
dependency: "direct main"
description:
path: "../../cherrypick_annotations"
relative: true
source: path
version: "1.1.1"
version: "1.1.2-dev.0"
cherrypick_flutter:
dependency: "direct main"
description:
path: "../../cherrypick_flutter"
relative: true
source: path
version: "1.1.3-dev.8"
version: "1.1.3-dev.9"
cherrypick_generator:
dependency: "direct dev"
description:
path: "../../cherrypick_generator"
relative: true
source: path
version: "1.1.1"
version: "2.0.0-dev.0"
clock:
dependency: transitive
description:

View File

@@ -4,7 +4,6 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:talker_flutter/talker_flutter.dart';
import 'domain/repository/post_repository.dart';
import 'presentation/bloc/post_bloc.dart';
import 'router/app_router.dart';
@@ -14,9 +13,11 @@ part 'app.inject.cherrypick.g.dart';
class TalkerProvider extends InheritedWidget {
final Talker talker;
const TalkerProvider({required this.talker, required super.child, super.key});
static Talker of(BuildContext context) => context.dependOnInheritedWidgetOfExactType<TalkerProvider>()!.talker;
static Talker of(BuildContext context) =>
context.dependOnInheritedWidgetOfExactType<TalkerProvider>()!.talker;
@override
bool updateShouldNotify(TalkerProvider oldWidget) => oldWidget.talker != talker;
bool updateShouldNotify(TalkerProvider oldWidget) =>
oldWidget.talker != talker;
}
@injectable()

View File

@@ -15,14 +15,16 @@ abstract class AppModule extends Module {
@provide()
@singleton()
TalkerDioLoggerSettings talkerDioLoggerSettings() => TalkerDioLoggerSettings(
printRequestHeaders: true,
printResponseHeaders: true,
printResponseMessage: true,
);
printRequestHeaders: true,
printResponseHeaders: true,
printResponseMessage: true,
);
@provide()
@singleton()
TalkerDioLogger talkerDioLogger(Talker talker, TalkerDioLoggerSettings settings) => TalkerDioLogger(talker: talker, settings: settings);
TalkerDioLogger talkerDioLogger(
Talker talker, TalkerDioLoggerSettings settings) =>
TalkerDioLogger(talker: talker, settings: settings);
@instance()
int timeout() => 1000;

View File

@@ -5,9 +5,9 @@ class CoreModule extends Module {
final Talker _talker;
CoreModule({required Talker talker}) : _talker = talker;
@override
void builder(Scope currentScope) {
bind<Talker>().toProvide(() => _talker).singleton();
}
}
}

View File

@@ -13,7 +13,6 @@ void main() {
final talker = Talker();
final talkerLogger = TalkerCherryPickObserver(talker);
Bloc.observer = TalkerBlocObserver(talker: talker);
CherryPick.setGlobalObserver(talkerLogger);
@@ -24,7 +23,10 @@ void main() {
}
// Используем safe root scope для гарантии защиты
CherryPick.openRootScope().installModules([CoreModule(talker: talker), $AppModule()]);
CherryPick.openRootScope()
.installModules([CoreModule(talker: talker), $AppModule()]);
runApp(MyApp(talker: talker,));
runApp(MyApp(
talker: talker,
));
}

View File

@@ -175,21 +175,21 @@ packages:
path: "../../cherrypick"
relative: true
source: path
version: "3.0.0-dev.8"
version: "3.0.0-dev.9"
cherrypick_annotations:
dependency: "direct main"
description:
path: "../../cherrypick_annotations"
relative: true
source: path
version: "1.1.1"
version: "1.1.2-dev.0"
cherrypick_generator:
dependency: "direct main"
description:
path: "../../cherrypick_generator"
relative: true
source: path
version: "1.1.1"
version: "2.0.0-dev.0"
cli_launcher:
dependency: transitive
description:
@@ -864,7 +864,7 @@ packages:
path: "../../talker_cherrypick_logger"
relative: true
source: path
version: "1.0.0"
version: "1.1.0-dev.3"
talker_dio_logger:
dependency: "direct main"
description:

View File

@@ -5,23 +5,23 @@ packages:
dependency: transitive
description:
name: _fe_analyzer_shared
sha256: "45cfa8471b89fb6643fe9bf51bd7931a76b8f5ec2d65de4fb176dba8d4f22c77"
sha256: "16e298750b6d0af7ce8a3ba7c18c69c3785d11b15ec83f6dcd0ad2a0009b3cab"
url: "https://pub.dev"
source: hosted
version: "73.0.0"
version: "76.0.0"
_macros:
dependency: transitive
description: dart
source: sdk
version: "0.3.2"
version: "0.3.3"
analyzer:
dependency: transitive
description:
name: analyzer
sha256: "4959fec185fe70cce007c57e9ab6983101dbe593d2bf8bbfb4453aaec0cf470a"
sha256: "1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e"
url: "https://pub.dev"
source: hosted
version: "6.8.0"
version: "6.11.0"
ansi_styles:
dependency: transitive
description:
@@ -298,10 +298,10 @@ packages:
dependency: transitive
description:
name: macros
sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536"
sha256: "1d9e801cd66f7ea3663c45fc708450db1fa57f988142c64289142c9b7ee80656"
url: "https://pub.dev"
source: hosted
version: "0.1.2-main.4"
version: "0.1.3-main.0"
matcher:
dependency: transitive
description:

View File

@@ -1,3 +1,7 @@
## 1.1.0-dev.4
- **DOCS**(readme): update install instructions to use pub.dev as default method and remove obsolete git example.
## 1.1.0-dev.3
## 1.1.0-dev.2

View File

@@ -21,15 +21,14 @@ All CherryPick lifecycle events, instance creations, cache operations, module ac
### 1. Add dependencies
Install the package **from [pub.dev](https://pub.dev/packages/talker_cherrypick_logger)**:
In your `pubspec.yaml`:
```yaml
dependencies:
cherrypick: ^latest
talker: ^latest
talker_cherrypick_logger:
git:
url: https://github.com/pese-dot-work/cherrypick.git
path: talker_cherrypick_logger
talker_cherrypick_logger: ^latest
```
### 2. Import the package

View File

@@ -69,26 +69,32 @@ class TalkerCherryPickObserver implements CherryPickObserver {
/// Called when a new instance is created.
@override
void onInstanceCreated(String name, Type type, Object instance, {String? scopeName}) {
talker.info('[create][CherryPick] $name$type => $instance (scope: $scopeName)');
void onInstanceCreated(String name, Type type, Object instance,
{String? scopeName}) {
talker.info(
'[create][CherryPick] $name$type => $instance (scope: $scopeName)');
}
/// Called when an instance is disposed.
@override
void onInstanceDisposed(String name, Type type, Object instance, {String? scopeName}) {
talker.info('[dispose][CherryPick] $name$type => $instance (scope: $scopeName)');
void onInstanceDisposed(String name, Type type, Object instance,
{String? scopeName}) {
talker.info(
'[dispose][CherryPick] $name$type => $instance (scope: $scopeName)');
}
/// Called when modules are installed.
@override
void onModulesInstalled(List<String> modules, {String? scopeName}) {
talker.info('[modules installed][CherryPick] ${modules.join(', ')} (scope: $scopeName)');
talker.info(
'[modules installed][CherryPick] ${modules.join(', ')} (scope: $scopeName)');
}
/// Called when modules are removed.
@override
void onModulesRemoved(List<String> modules, {String? scopeName}) {
talker.info('[modules removed][CherryPick] ${modules.join(', ')} (scope: $scopeName)');
talker.info(
'[modules removed][CherryPick] ${modules.join(', ')} (scope: $scopeName)');
}
/// Called when a DI scope is opened.
@@ -106,7 +112,8 @@ class TalkerCherryPickObserver implements CherryPickObserver {
/// Called if the DI container detects a cycle in the dependency graph.
@override
void onCycleDetected(List<String> chain, {String? scopeName}) {
talker.warning('[cycle][CherryPick] Detected: ${chain.join(' -> ')}${scopeName != null ? ' (scope: $scopeName)' : ''}');
talker.warning(
'[cycle][CherryPick] Detected: ${chain.join(' -> ')}${scopeName != null ? ' (scope: $scopeName)' : ''}');
}
/// Called when an instance is found in the cache.
@@ -136,6 +143,7 @@ class TalkerCherryPickObserver implements CherryPickObserver {
/// Called for error events with optional stack trace.
@override
void onError(String message, Object? error, StackTrace? stackTrace) {
talker.handle(error ?? '[CherryPick] $message', stackTrace, '[error][CherryPick] $message');
talker.handle(error ?? '[CherryPick] $message', stackTrace,
'[error][CherryPick] $message');
}
}
}

View File

@@ -1,6 +1,6 @@
name: talker_cherrypick_logger
description: A Talker logger integration for CherryPick DI to observe and log DI events and errors.
version: 1.1.0-dev.3
version: 1.1.0-dev.4
homepage: https://pese-git.github.io/cherrypick-site/
documentation: https://github.com/pese-git/cherrypick/wiki
repository: https://github.com/pese-git/cherrypick

View File

@@ -15,7 +15,8 @@ void main() {
test('onInstanceRequested logs info', () {
observer.onInstanceRequested('A', String, scopeName: 'test');
final log = talker.history.last;
expect(log.message, contains('[request][CherryPick] A — String (scope: test)'));
expect(log.message,
contains('[request][CherryPick] A — String (scope: test)'));
});
test('onCycleDetected logs warning', () {