Compare commits

...

10 Commits

Author SHA1 Message Date
Sergey Penkovsky
a4c5fd922e chore(release): publish packages
- cherrypick@3.0.0-dev.10
 - cherrypick_annotations@1.1.2-dev.1
 - cherrypick_flutter@1.1.3-dev.10
 - cherrypick_generator@2.0.0-dev.1
 - talker_cherrypick_logger@1.1.0-dev.5
2025-08-15 09:06:46 +03:00
Sergey Penkovsky
8870b8ce54 docs(pub): update homepage and documentation URLs in pubspec.yaml to new official site 2025-08-15 09:04:39 +03:00
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
Sergey Penkovsky
99e662124f chore(release): publish packages
- talker_cherrypick_logger@1.1.0-dev.3
2025-08-13 15:27:51 +03:00
Sergey Penkovsky
03f54981f3 chore(talker_cherrypick_logger): update package description in pubspec.yaml 2025-08-13 15:26:53 +03:00
Sergey Penkovsky
349efe6ba6 chore(release): publish packages
- talker_cherrypick_logger@1.1.0-dev.2
2025-08-13 15:23:21 +03:00
Sergey Penkovsky
c2f0e027b6 fix(gitignore) - update gitignore 2025-08-13 15:18:39 +03:00
59 changed files with 917 additions and 491 deletions

View File

@@ -3,6 +3,108 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## 2025-08-15
### Changes
---
Packages with breaking changes:
- There are no breaking changes in this release.
Packages with other changes:
- [`cherrypick` - `v3.0.0-dev.10`](#cherrypick---v300-dev10)
- [`cherrypick_annotations` - `v1.1.2-dev.1`](#cherrypick_annotations---v112-dev1)
- [`cherrypick_flutter` - `v1.1.3-dev.10`](#cherrypick_flutter---v113-dev10)
- [`cherrypick_generator` - `v2.0.0-dev.1`](#cherrypick_generator---v200-dev1)
- [`talker_cherrypick_logger` - `v1.1.0-dev.5`](#talker_cherrypick_logger---v110-dev5)
---
#### `cherrypick` - `v3.0.0-dev.10`
- **DOCS**(pub): update homepage and documentation URLs in pubspec.yaml to new official site.
#### `cherrypick_annotations` - `v1.1.2-dev.1`
- **DOCS**(pub): update homepage and documentation URLs in pubspec.yaml to new official site.
#### `cherrypick_flutter` - `v1.1.3-dev.10`
- **DOCS**(pub): update homepage and documentation URLs in pubspec.yaml to new official site.
#### `cherrypick_generator` - `v2.0.0-dev.1`
- **DOCS**(pub): update homepage and documentation URLs in pubspec.yaml to new official site.
#### `talker_cherrypick_logger` - `v1.1.0-dev.5`
- **DOCS**(pub): update homepage and documentation URLs in pubspec.yaml to new official site.
## 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
---
Packages with breaking changes:
- There are no breaking changes in this release.
Packages with other changes:
- [`talker_cherrypick_logger` - `v1.1.0-dev.3`](#talker_cherrypick_logger---v110-dev3)
---
#### `talker_cherrypick_logger` - `v1.1.0-dev.3`
## 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.2`](#talker_cherrypick_logger---v110-dev2)
---
#### `talker_cherrypick_logger` - `v1.1.0-dev.2`
- Bump "talker_cherrypick_logger" to `1.1.0-dev.2`.
## 2025-08-13 ## 2025-08-13
### Changes ### Changes

View File

@@ -73,7 +73,8 @@ class UniversalChainBenchmark<TContainer> extends BenchmarkBase {
_childDi!.resolve<UniversalService>(); _childDi!.resolve<UniversalService>();
break; break;
case UniversalScenario.asyncChain: 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') { if (config.di == 'getit') {
final di = GetItAdapter(); final di = GetItAdapter();
if (scenario == UniversalScenario.asyncChain) { if (scenario == UniversalScenario.asyncChain) {
final benchAsync = UniversalChainAsyncBenchmark<GetIt>(di, final benchAsync = UniversalChainAsyncBenchmark<GetIt>(
chainCount: c, nestingDepth: d, mode: mode, di,
chainCount: c,
nestingDepth: d,
mode: mode,
); );
benchResult = await BenchmarkRunner.runAsync( benchResult = await BenchmarkRunner.runAsync(
benchmark: benchAsync, benchmark: benchAsync,
@@ -45,8 +48,12 @@ class BenchmarkCliRunner {
repeats: config.repeats, repeats: config.repeats,
); );
} else { } else {
final benchSync = UniversalChainBenchmark<GetIt>(di, final benchSync = UniversalChainBenchmark<GetIt>(
chainCount: c, nestingDepth: d, mode: mode, scenario: scenario, di,
chainCount: c,
nestingDepth: d,
mode: mode,
scenario: scenario,
); );
benchResult = await BenchmarkRunner.runSync( benchResult = await BenchmarkRunner.runSync(
benchmark: benchSync, benchmark: benchSync,
@@ -57,8 +64,12 @@ class BenchmarkCliRunner {
} else if (config.di == 'riverpod') { } else if (config.di == 'riverpod') {
final di = RiverpodAdapter(); final di = RiverpodAdapter();
if (scenario == UniversalScenario.asyncChain) { if (scenario == UniversalScenario.asyncChain) {
final benchAsync = UniversalChainAsyncBenchmark<Map<String, rp.ProviderBase<Object?>>>(di, final benchAsync = UniversalChainAsyncBenchmark<
chainCount: c, nestingDepth: d, mode: mode, Map<String, rp.ProviderBase<Object?>>>(
di,
chainCount: c,
nestingDepth: d,
mode: mode,
); );
benchResult = await BenchmarkRunner.runAsync( benchResult = await BenchmarkRunner.runAsync(
benchmark: benchAsync, benchmark: benchAsync,
@@ -66,8 +77,13 @@ class BenchmarkCliRunner {
repeats: config.repeats, repeats: config.repeats,
); );
} else { } else {
final benchSync = UniversalChainBenchmark<Map<String, rp.ProviderBase<Object?>>>(di, final benchSync = UniversalChainBenchmark<
chainCount: c, nestingDepth: d, mode: mode, scenario: scenario, Map<String, rp.ProviderBase<Object?>>>(
di,
chainCount: c,
nestingDepth: d,
mode: mode,
scenario: scenario,
); );
benchResult = await BenchmarkRunner.runSync( benchResult = await BenchmarkRunner.runSync(
benchmark: benchSync, benchmark: benchSync,
@@ -78,8 +94,11 @@ class BenchmarkCliRunner {
} else { } else {
final di = CherrypickDIAdapter(); final di = CherrypickDIAdapter();
if (scenario == UniversalScenario.asyncChain) { if (scenario == UniversalScenario.asyncChain) {
final benchAsync = UniversalChainAsyncBenchmark<Scope>(di, final benchAsync = UniversalChainAsyncBenchmark<Scope>(
chainCount: c, nestingDepth: d, mode: mode, di,
chainCount: c,
nestingDepth: d,
mode: mode,
); );
benchResult = await BenchmarkRunner.runAsync( benchResult = await BenchmarkRunner.runAsync(
benchmark: benchAsync, benchmark: benchAsync,
@@ -87,8 +106,12 @@ class BenchmarkCliRunner {
repeats: config.repeats, repeats: config.repeats,
); );
} else { } else {
final benchSync = UniversalChainBenchmark<Scope>(di, final benchSync = UniversalChainBenchmark<Scope>(
chainCount: c, nestingDepth: d, mode: mode, scenario: scenario, di,
chainCount: c,
nestingDepth: d,
mode: mode,
scenario: scenario,
); );
benchResult = await BenchmarkRunner.runSync( benchResult = await BenchmarkRunner.runSync(
benchmark: benchSync, benchmark: benchSync,
@@ -103,7 +126,11 @@ class BenchmarkCliRunner {
var median = timings[timings.length ~/ 2]; var median = timings[timings.length ~/ 2];
var minVal = timings.first; var minVal = timings.first;
var maxVal = timings.last; 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({ results.add({
'benchmark': 'Universal_$bench', 'benchmark': 'Universal_$bench',
'chainCount': c, 'chainCount': c,
@@ -128,6 +155,7 @@ class BenchmarkCliRunner {
'json': JsonReport(), 'json': JsonReport(),
'markdown': MarkdownReport(), '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 { enum UniversalBenchmark {
/// Simple singleton registration benchmark /// Simple singleton registration benchmark
registerSingleton, registerSingleton,
/// Chain of singleton dependencies /// Chain of singleton dependencies
chainSingleton, chainSingleton,
/// Chain using factories /// Chain using factories
chainFactory, chainFactory,
/// Async chain resolution /// Async chain resolution
chainAsync, chainAsync,
/// Named registration benchmark /// Named registration benchmark
named, named,
/// Override/child-scope benchmark /// Override/child-scope benchmark
override, override,
} }
@@ -65,23 +70,32 @@ T parseEnum<T>(String value, List<T> values, T defaultValue) {
} }
/// Parses comma-separated integer list from [s]. /// Parses comma-separated integer list from [s].
List<int> parseIntList(String s) => List<int> parseIntList(String s) => s
s.split(',').map((e) => int.tryParse(e.trim()) ?? 0).where((x) => x > 0).toList(); .split(',')
.map((e) => int.tryParse(e.trim()) ?? 0)
.where((x) => x > 0)
.toList();
/// CLI config describing what and how to benchmark. /// CLI config describing what and how to benchmark.
class BenchmarkCliConfig { class BenchmarkCliConfig {
/// Benchmarks enabled to run (scenarios). /// Benchmarks enabled to run (scenarios).
final List<UniversalBenchmark> benchesToRun; final List<UniversalBenchmark> benchesToRun;
/// List of chain counts (parallel, per test). /// List of chain counts (parallel, per test).
final List<int> chainCounts; final List<int> chainCounts;
/// List of nesting depths (max chain length, per test). /// List of nesting depths (max chain length, per test).
final List<int> nestDepths; final List<int> nestDepths;
/// How many times to repeat each trial. /// How many times to repeat each trial.
final int repeats; final int repeats;
/// How many times to warm-up before measuring. /// How many times to warm-up before measuring.
final int warmups; final int warmups;
/// Output report format. /// Output report format.
final String format; final String format;
/// Name of DI implementation ("cherrypick" or "getit") /// Name of DI implementation ("cherrypick" or "getit")
final String di; final String di;
BenchmarkCliConfig({ BenchmarkCliConfig({
@@ -105,7 +119,9 @@ BenchmarkCliConfig parseBenchmarkCli(List<String> args) {
..addOption('repeat', abbr: 'r', defaultsTo: '2') ..addOption('repeat', abbr: 'r', defaultsTo: '2')
..addOption('warmup', abbr: 'w', defaultsTo: '1') ..addOption('warmup', abbr: 'w', defaultsTo: '1')
..addOption('format', abbr: 'f', defaultsTo: 'pretty') ..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'); ..addFlag('help', abbr: 'h', negatable: false, help: 'Show help');
final result = parser.parse(args); final result = parser.parse(args);
if (result['help'] == true) { if (result['help'] == true) {

View File

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

View File

@@ -5,6 +5,7 @@ class JsonReport extends ReportGenerator {
/// No specific keys; outputs all fields in raw map. /// No specific keys; outputs all fields in raw map.
@override @override
List<String> get keys => []; List<String> get keys => [];
/// Renders all result rows as a pretty-printed JSON array. /// Renders all result rows as a pretty-printed JSON array.
@override @override
String render(List<Map<String, dynamic>> rows) { String render(List<Map<String, dynamic>> rows) {

View File

@@ -7,25 +7,46 @@ class MarkdownReport extends ReportGenerator {
/// List of columns (keys) to show in the Markdown table. /// List of columns (keys) to show in the Markdown table.
@override @override
final List<String> keys = [ final List<String> keys = [
'benchmark','chainCount','nestingDepth','mean_us','median_us','stddev_us', 'benchmark',
'min_us','max_us','trials','memory_diff_kb','delta_peak_kb','peak_rss_kb' '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. /// Friendly display names for each benchmark type.
static const nameMap = { static const nameMap = {
'Universal_UniversalBenchmark.registerSingleton':'RegisterSingleton', 'Universal_UniversalBenchmark.registerSingleton': 'RegisterSingleton',
'Universal_UniversalBenchmark.chainSingleton':'ChainSingleton', 'Universal_UniversalBenchmark.chainSingleton': 'ChainSingleton',
'Universal_UniversalBenchmark.chainFactory':'ChainFactory', 'Universal_UniversalBenchmark.chainFactory': 'ChainFactory',
'Universal_UniversalBenchmark.chainAsync':'AsyncChain', 'Universal_UniversalBenchmark.chainAsync': 'AsyncChain',
'Universal_UniversalBenchmark.named':'Named', 'Universal_UniversalBenchmark.named': 'Named',
'Universal_UniversalBenchmark.override':'Override', 'Universal_UniversalBenchmark.override': 'Override',
}; };
/// Renders all results as a formatted Markdown table with aligned columns and a legend. /// Renders all results as a formatted Markdown table with aligned columns and a legend.
@override @override
String render(List<Map<String, dynamic>> rows) { String render(List<Map<String, dynamic>> rows) {
final headers = [ 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 dataRows = rows.map((r) {
final readableName = nameMap[r['benchmark']] ?? r['benchmark']; final readableName = nameMap[r['benchmark']] ?? r['benchmark'];
@@ -73,6 +94,6 @@ class MarkdownReport extends ReportGenerator {
> `PeakRSS(KB)` Max observed RSS memory (KB) > `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,8 +7,18 @@ class PrettyReport extends ReportGenerator {
/// List of columns to output in the pretty report. /// List of columns to output in the pretty report.
@override @override
final List<String> keys = [ final List<String> keys = [
'benchmark','chainCount','nestingDepth','mean_us','median_us','stddev_us', 'benchmark',
'min_us','max_us','trials','memory_diff_kb','delta_peak_kb','peak_rss_kb' '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. /// Mappings from internal benchmark IDs to display names.
@@ -25,7 +35,18 @@ class PrettyReport extends ReportGenerator {
@override @override
String render(List<Map<String, dynamic>> rows) { String render(List<Map<String, dynamic>> rows) {
final headers = [ 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 header = headers.join('\t');
final lines = rows.map((r) { final lines = rows.map((r) {

View File

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

View File

@@ -7,10 +7,13 @@ import 'package:benchmark_di/benchmarks/universal_chain_async_benchmark.dart';
class BenchmarkResult { class BenchmarkResult {
/// List of timings for each run (in microseconds). /// List of timings for each run (in microseconds).
final List<num> timings; final List<num> timings;
/// Difference in memory (RSS, in KB) after running. /// Difference in memory (RSS, in KB) after running.
final int memoryDiffKb; final int memoryDiffKb;
/// Difference between peak RSS and initial RSS (in KB). /// Difference between peak RSS and initial RSS (in KB).
final int deltaPeakKb; final int deltaPeakKb;
/// Peak RSS memory observed (in KB). /// Peak RSS memory observed (in KB).
final int peakRssKb; final int peakRssKb;
BenchmarkResult({ BenchmarkResult({
@@ -19,6 +22,7 @@ class BenchmarkResult {
required this.deltaPeakKb, required this.deltaPeakKb,
required this.peakRssKb, required this.peakRssKb,
}); });
/// Computes a BenchmarkResult instance from run timings and memory data. /// Computes a BenchmarkResult instance from run timings and memory data.
factory BenchmarkResult.collect({ factory BenchmarkResult.collect({
required List<num> timings, required List<num> timings,
@@ -64,7 +68,8 @@ class BenchmarkRunner {
rssValues.add(ProcessInfo.currentRss); rssValues.add(ProcessInfo.currentRss);
benchmark.teardown(); 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]. /// Runs an asynchronous benchmark ([UniversalChainAsyncBenchmark]) for a given number of [warmups] and [repeats].
@@ -91,6 +96,7 @@ class BenchmarkRunner {
rssValues.add(ProcessInfo.currentRss); rssValues.add(ProcessInfo.currentRss);
await benchmark.teardown(); 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 'package:cherrypick/cherrypick.dart';
import 'di_adapter.dart'; import 'di_adapter.dart';
/// Test module that generates a chain of service bindings for benchmarking. /// Test module that generates a chain of service bindings for benchmarking.
/// ///
/// Configurable by chain count, nesting depth, binding mode, and scenario /// Configurable by chain count, nesting depth, binding mode, and scenario
@@ -12,10 +11,13 @@ import 'di_adapter.dart';
class UniversalChainModule extends Module { class UniversalChainModule extends Module {
/// Number of chains to create. /// Number of chains to create.
final int chainCount; final int chainCount;
/// Depth of each chain. /// Depth of each chain.
final int nestingDepth; final int nestingDepth;
/// How modules are registered (factory/singleton/async). /// How modules are registered (factory/singleton/async).
final UniversalBindingMode bindingMode; final UniversalBindingMode bindingMode;
/// Which di scenario to generate (chained, named, etc). /// Which di scenario to generate (chained, named, etc).
final UniversalScenario scenario; final UniversalScenario scenario;
@@ -40,7 +42,8 @@ class UniversalChainModule extends Module {
bind<UniversalService>() bind<UniversalService>()
.toProvideAsync(() async { .toProvideAsync(() async {
final prev = level > 1 final prev = level > 1
? await currentScope.resolveAsync<UniversalService>(named: prevDepName) ? await currentScope.resolveAsync<UniversalService>(
named: prevDepName)
: null; : null;
return UniversalServiceImpl( return UniversalServiceImpl(
value: depName, value: depName,
@@ -58,13 +61,18 @@ class UniversalChainModule extends Module {
case UniversalScenario.register: case UniversalScenario.register:
// Simple singleton registration. // Simple singleton registration.
bind<UniversalService>() bind<UniversalService>()
.toProvide(() => UniversalServiceImpl(value: 'reg', dependency: null)) .toProvide(
() => UniversalServiceImpl(value: 'reg', dependency: null))
.singleton(); .singleton();
break; break;
case UniversalScenario.named: case UniversalScenario.named:
// Named factory registration for two distinct objects. // Named factory registration for two distinct objects.
bind<UniversalService>().toProvide(() => UniversalServiceImpl(value: 'impl1')).withName('impl1'); bind<UniversalService>()
bind<UniversalService>().toProvide(() => UniversalServiceImpl(value: 'impl2')).withName('impl2'); .toProvide(() => UniversalServiceImpl(value: 'impl1'))
.withName('impl1');
bind<UniversalService>()
.toProvide(() => UniversalServiceImpl(value: 'impl2'))
.withName('impl2');
break; break;
case UniversalScenario.chain: case UniversalScenario.chain:
// Chain of nested services, with dependency on previous level by name. // Chain of nested services, with dependency on previous level by name.
@@ -79,7 +87,8 @@ class UniversalChainModule extends Module {
bind<UniversalService>() bind<UniversalService>()
.toProvide(() => UniversalServiceImpl( .toProvide(() => UniversalServiceImpl(
value: depName, value: depName,
dependency: currentScope.tryResolve<UniversalService>(named: prevDepName), dependency: currentScope.tryResolve<UniversalService>(
named: prevDepName),
)) ))
.withName(depName) .withName(depName)
.singleton(); .singleton();
@@ -88,7 +97,8 @@ class UniversalChainModule extends Module {
bind<UniversalService>() bind<UniversalService>()
.toProvide(() => UniversalServiceImpl( .toProvide(() => UniversalServiceImpl(
value: depName, value: depName,
dependency: currentScope.tryResolve<UniversalService>(named: prevDepName), dependency: currentScope.tryResolve<UniversalService>(
named: prevDepName),
)) ))
.withName(depName); .withName(depName);
break; break;
@@ -96,7 +106,9 @@ class UniversalChainModule extends Module {
bind<UniversalService>() bind<UniversalService>()
.toProvideAsync(() async => UniversalServiceImpl( .toProvideAsync(() async => UniversalServiceImpl(
value: depName, value: depName,
dependency: await currentScope.resolveAsync<UniversalService>(named: prevDepName), dependency:
await currentScope.resolveAsync<UniversalService>(
named: prevDepName),
)) ))
.withName(depName) .withName(depName)
.singleton(); .singleton();
@@ -107,14 +119,16 @@ class UniversalChainModule extends Module {
// Регистрация алиаса без имени (на последний элемент цепочки) // Регистрация алиаса без имени (на последний элемент цепочки)
final depName = '${chainCount}_$nestingDepth'; final depName = '${chainCount}_$nestingDepth';
bind<UniversalService>() bind<UniversalService>()
.toProvide(() => currentScope.resolve<UniversalService>(named: depName)) .toProvide(
() => currentScope.resolve<UniversalService>(named: depName))
.singleton(); .singleton();
break; break;
case UniversalScenario.override: case UniversalScenario.override:
// handled at benchmark level, но алиас нужен прямо в этом scope! // handled at benchmark level, но алиас нужен прямо в этом scope!
final depName = '${chainCount}_$nestingDepth'; final depName = '${chainCount}_$nestingDepth';
bind<UniversalService>() bind<UniversalService>()
.toProvide(() => currentScope.resolve<UniversalService>(named: depName)) .toProvide(
() => currentScope.resolve<UniversalService>(named: depName))
.singleton(); .singleton();
break; break;
case UniversalScenario.asyncChain: case UniversalScenario.asyncChain:
@@ -124,7 +138,6 @@ class UniversalChainModule extends Module {
} }
} }
class CherrypickDIAdapter extends DIAdapter<Scope> { class CherrypickDIAdapter extends DIAdapter<Scope> {
Scope? _scope; Scope? _scope;
final bool _isSubScope; 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 @override

View File

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

View File

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

View File

@@ -20,7 +20,9 @@ class RiverpodAdapter extends DIAdapter<Map<String, rp.ProviderBase<Object?>>> {
_parent = parent; _parent = parent;
@override @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 _container ??= _parent == null
? rp.ProviderContainer() ? rp.ProviderContainer()
: rp.ProviderContainer(parent: _parent); : rp.ProviderContainer(parent: _parent);
@@ -76,7 +78,8 @@ class RiverpodAdapter extends DIAdapter<Map<String, rp.ProviderBase<Object?>>> {
} }
@override @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 S scenario,
required int chainCount, required int chainCount,
required int nestingDepth, required int nestingDepth,
@@ -86,25 +89,34 @@ class RiverpodAdapter extends DIAdapter<Map<String, rp.ProviderBase<Object?>>> {
return (providers) { return (providers) {
switch (scenario) { switch (scenario) {
case UniversalScenario.register: 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; break;
case UniversalScenario.named: case UniversalScenario.named:
providers['impl1'] = rp.Provider<UniversalService>((ref) => UniversalServiceImpl(value: 'impl1')); providers['impl1'] = rp.Provider<UniversalService>(
providers['impl2'] = rp.Provider<UniversalService>((ref) => UniversalServiceImpl(value: 'impl2')); (ref) => UniversalServiceImpl(value: 'impl1'));
providers['impl2'] = rp.Provider<UniversalService>(
(ref) => UniversalServiceImpl(value: 'impl2'));
break; break;
case UniversalScenario.chain: case UniversalScenario.chain:
for (int chain = 1; chain <= chainCount; chain++) { for (int chain = 1; chain <= chainCount; chain++) {
for (int level = 1; level <= nestingDepth; level++) { for (int level = 1; level <= nestingDepth; level++) {
final prevDepName = '${chain}_${level - 1}'; final prevDepName = '${chain}_${level - 1}';
final depName = '${chain}_$level'; final depName = '${chain}_$level';
providers[depName] = rp.Provider<UniversalService>((ref) => UniversalServiceImpl( providers[depName] =
rp.Provider<UniversalService>((ref) => UniversalServiceImpl(
value: depName, value: depName,
dependency: level > 1 ? ref.watch(providers[prevDepName] as rp.ProviderBase<UniversalService>) : null, dependency: level > 1
? ref.watch(providers[prevDepName]
as rp.ProviderBase<UniversalService>)
: null,
)); ));
} }
} }
final depName = '${chainCount}_$nestingDepth'; 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; break;
case UniversalScenario.override: case UniversalScenario.override:
// handled at benchmark level // handled at benchmark level
@@ -114,24 +126,31 @@ class RiverpodAdapter extends DIAdapter<Map<String, rp.ProviderBase<Object?>>> {
for (int level = 1; level <= nestingDepth; level++) { for (int level = 1; level <= nestingDepth; level++) {
final prevDepName = '${chain}_${level - 1}'; final prevDepName = '${chain}_${level - 1}';
final depName = '${chain}_$level'; final depName = '${chain}_$level';
providers[depName] = rp.FutureProvider<UniversalService>((ref) async { providers[depName] =
rp.FutureProvider<UniversalService>((ref) async {
return UniversalServiceImpl( return UniversalServiceImpl(
value: depName, value: depName,
dependency: level > 1 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, : null,
); );
}); });
} }
} }
final depName = '${chainCount}_$nestingDepth'; final depName = '${chainCount}_$nestingDepth';
providers['UniversalService'] = rp.FutureProvider<UniversalService>((ref) async { providers['UniversalService'] =
return await ref.watch((providers[depName] as rp.FutureProvider<UniversalService>).future); rp.FutureProvider<UniversalService>((ref) async {
return await ref.watch(
(providers[depName] as rp.FutureProvider<UniversalService>)
.future);
}); });
break; 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 { enum UniversalScenario {
/// Single registration. /// Single registration.
register, register,
/// Chain of dependencies. /// Chain of dependencies.
chain, chain,
/// Named registrations. /// Named registrations.
named, named,
/// Child-scope override scenario. /// Child-scope override scenario.
override, override,
/// Asynchronous chain scenario. /// Asynchronous chain scenario.
asyncChain, asyncChain,
} }

View File

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

View File

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

View File

@@ -1,3 +1,7 @@
## 3.0.0-dev.10
- **DOCS**(pub): update homepage and documentation URLs in pubspec.yaml to new official site.
## 3.0.0-dev.9 ## 3.0.0-dev.9
- **DOCS**(readme): add talker_cherrypick_logger to Additional Modules section. - **DOCS**(readme): add talker_cherrypick_logger to Additional Modules section.

View File

@@ -36,18 +36,16 @@ class DatabaseModule extends Module {
class ApiModule extends Module { class ApiModule extends Module {
@override @override
void builder(Scope currentScope) { void builder(Scope currentScope) {
bind<ApiService>().toProvide(() => ApiService( bind<ApiService>()
currentScope.resolve<DatabaseService>() .toProvide(() => ApiService(currentScope.resolve<DatabaseService>()));
));
} }
} }
class UserModule extends Module { class UserModule extends Module {
@override @override
void builder(Scope currentScope) { void builder(Scope currentScope) {
bind<UserService>().toProvide(() => UserService( bind<UserService>()
currentScope.resolve<ApiService>() .toProvide(() => UserService(currentScope.resolve<ApiService>()));
));
} }
} }
@@ -65,18 +63,16 @@ class CircularServiceB {
class CircularModuleA extends Module { class CircularModuleA extends Module {
@override @override
void builder(Scope currentScope) { void builder(Scope currentScope) {
bind<CircularServiceA>().toProvide(() => CircularServiceA( bind<CircularServiceA>().toProvide(
currentScope.resolve<CircularServiceB>() () => CircularServiceA(currentScope.resolve<CircularServiceB>()));
));
} }
} }
class CircularModuleB extends Module { class CircularModuleB extends Module {
@override @override
void builder(Scope currentScope) { void builder(Scope currentScope) {
bind<CircularServiceB>().toProvide(() => CircularServiceB( bind<CircularServiceB>().toProvide(
currentScope.resolve<CircularServiceA>() () => CircularServiceB(currentScope.resolve<CircularServiceA>()));
));
} }
} }
@@ -87,11 +83,13 @@ void main() {
print('1. Globally enable cycle detection:'); print('1. Globally enable cycle detection:');
CherryPick.enableGlobalCycleDetection(); 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 // All new scopes will automatically have cycle detection enabled
final globalScope = CherryPick.openRootScope(); 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 // Install modules without circular dependencies
globalScope.installModules([ globalScope.installModules([
@@ -112,7 +110,8 @@ void main() {
// Создаем безопасный скоуп (с автоматически включенным обнаружением) // Создаем безопасный скоуп (с автоматически включенным обнаружением)
final safeScope = CherryPick.openSafeRootScope(); final safeScope = CherryPick.openSafeRootScope();
print('✅ Safe scope created with cycle detection: ${safeScope.isCycleDetectionEnabled}'); print(
'✅ Safe scope created with cycle detection: ${safeScope.isCycleDetectionEnabled}');
safeScope.installModules([ safeScope.installModules([
DatabaseModule(), DatabaseModule(),
@@ -153,30 +152,37 @@ void main() {
// Создаем скоуп без обнаружения // Создаем скоуп без обнаружения
// ignore: unused_local_variable // ignore: unused_local_variable
final specificScope = CherryPick.openRootScope(); final specificScope = CherryPick.openRootScope();
print(' Detection in root scope: ${CherryPick.isCycleDetectionEnabledForScope()}'); print(
' Detection in root scope: ${CherryPick.isCycleDetectionEnabledForScope()}');
// Включаем обнаружение для конкретного скоупа // Включаем обнаружение для конкретного скоупа
CherryPick.enableCycleDetectionForScope(); CherryPick.enableCycleDetectionForScope();
print('✅ Detection enabled for root scope: ${CherryPick.isCycleDetectionEnabledForScope()}'); print(
'✅ Detection enabled for root scope: ${CherryPick.isCycleDetectionEnabledForScope()}');
// Создаем дочерний скоуп // Создаем дочерний скоуп
// ignore: unused_local_variable // ignore: unused_local_variable
final featureScope = CherryPick.openScope(scopeName: 'feature.auth'); 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'); 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(''); print('');
// Example 5: Creating safe child scopes // Example 5: Creating safe child scopes
print('5. Creating safe child scopes:'); print('5. Creating safe child scopes:');
final safeFeatureScope = CherryPick.openSafeScope(scopeName: 'feature.payments'); final safeFeatureScope =
print('✅ Safe feature scope created: ${safeFeatureScope.isCycleDetectionEnabled}'); CherryPick.openSafeScope(scopeName: 'feature.payments');
print(
'✅ Safe feature scope created: ${safeFeatureScope.isCycleDetectionEnabled}');
// You can create a complex hierarchy of scopes // 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('✅ Complex scope created: ${complexScope.isCycleDetectionEnabled}');
print(''); print('');
@@ -209,7 +215,8 @@ void main() {
print(''); print('');
print('🚀 Production mode:'); print('🚀 Production mode:');
print(' CherryPick.disableGlobalCycleDetection(); // Disable for performance'); print(
' CherryPick.disableGlobalCycleDetection(); // Disable for performance');
print(' final scope = CherryPick.openRootScope(); // Regular scope'); print(' final scope = CherryPick.openRootScope(); // Regular scope');
print(''); print('');
@@ -219,7 +226,8 @@ void main() {
print(''); print('');
print('🎯 Feature-specific:'); print('🎯 Feature-specific:');
print(' CherryPick.enableCycleDetectionForScope(scopeName: "feature.critical");'); print(
' CherryPick.enableCycleDetectionForScope(scopeName: "feature.critical");');
print(' // Enable only for critical features'); print(' // Enable only for critical features');
// Cleanup // Cleanup

View File

@@ -29,18 +29,16 @@ class OrderService {
class UserModule extends Module { class UserModule extends Module {
@override @override
void builder(Scope currentScope) { void builder(Scope currentScope) {
bind<UserService>().toProvide(() => UserService( bind<UserService>()
currentScope.resolve<OrderService>() .toProvide(() => UserService(currentScope.resolve<OrderService>()));
));
} }
} }
class OrderModule extends Module { class OrderModule extends Module {
@override @override
void builder(Scope currentScope) { void builder(Scope currentScope) {
bind<OrderService>().toProvide(() => OrderService( bind<OrderService>()
currentScope.resolve<UserService>() .toProvide(() => OrderService(currentScope.resolve<UserService>()));
));
} }
} }
@@ -103,9 +101,8 @@ class ImprovedUserModule extends Module {
@override @override
void builder(Scope currentScope) { void builder(Scope currentScope) {
bind<UserRepository>().singleton().toProvide(() => UserRepository()); bind<UserRepository>().singleton().toProvide(() => UserRepository());
bind<ImprovedUserService>().toProvide(() => ImprovedUserService( bind<ImprovedUserService>().toProvide(
currentScope.resolve<UserRepository>() () => ImprovedUserService(currentScope.resolve<UserRepository>()));
));
} }
} }
@@ -115,8 +112,7 @@ class ImprovedOrderModule extends Module {
bind<OrderRepository>().singleton().toProvide(() => OrderRepository()); bind<OrderRepository>().singleton().toProvide(() => OrderRepository());
bind<ImprovedOrderService>().toProvide(() => ImprovedOrderService( bind<ImprovedOrderService>().toProvide(() => ImprovedOrderService(
currentScope.resolve<OrderRepository>(), currentScope.resolve<OrderRepository>(),
currentScope.resolve<ImprovedUserService>() currentScope.resolve<ImprovedUserService>()));
));
} }
} }
@@ -127,7 +123,8 @@ void main() {
print('1. Attempt to create a scope with circular dependencies:'); print('1. Attempt to create a scope with circular dependencies:');
try { try {
final scope = CherryPick.openRootScope(); final scope = CherryPick.openRootScope();
scope.enableCycleDetection(); // Включаем обнаружение циклических зависимостей scope
.enableCycleDetection(); // Включаем обнаружение циклических зависимостей
scope.installModules([ scope.installModules([
UserModule(), UserModule(),
@@ -184,7 +181,6 @@ void main() {
orderService.createOrder('ORD-001', 'John'); orderService.createOrder('ORD-001', 'John');
final orders = orderService.getOrdersForUser('John'); final orders = orderService.getOrdersForUser('John');
print('✅ Orders for user John: $orders'); print('✅ Orders for user John: $orders');
} catch (e) { } catch (e) {
print('❌ Error: $e'); print('❌ Error: $e');
} }

View File

@@ -77,7 +77,8 @@ class CycleDetector {
); );
if (_resolutionStack.contains(dependencyKey)) { if (_resolutionStack.contains(dependencyKey)) {
final cycleStartIndex = _resolutionHistory.indexOf(dependencyKey); final cycleStartIndex = _resolutionHistory.indexOf(dependencyKey);
final cycle = _resolutionHistory.sublist(cycleStartIndex)..add(dependencyKey); final cycle = _resolutionHistory.sublist(cycleStartIndex)
..add(dependencyKey);
_observer.onCycleDetected(cycle); _observer.onCycleDetected(cycle);
_observer.onError('Cycle detected for $dependencyKey', null, null); _observer.onError('Cycle detected for $dependencyKey', null, null);
throw CircularDependencyException( throw CircularDependencyException(
@@ -99,7 +100,8 @@ class CycleDetector {
); );
_resolutionStack.remove(dependencyKey); _resolutionStack.remove(dependencyKey);
// Only remove from history if it's the last one // Only remove from history if it's the last one
if (_resolutionHistory.isNotEmpty && _resolutionHistory.last == dependencyKey) { if (_resolutionHistory.isNotEmpty &&
_resolutionHistory.last == dependencyKey) {
_resolutionHistory.removeLast(); _resolutionHistory.removeLast();
} }
} }
@@ -124,7 +126,8 @@ class CycleDetector {
} }
/// Gets the current dependency resolution chain (for diagnostics or debugging). /// 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). /// Returns a unique string key for type [T] (+name).
String _createDependencyKey<T>(String? named) { String _createDependencyKey<T>(String? named) {
@@ -205,7 +208,8 @@ mixin CycleDetectionMixin {
: dependencyType.toString(); : dependencyType.toString();
if (_cycleDetector!._resolutionStack.contains(dependencyKey)) { if (_cycleDetector!._resolutionStack.contains(dependencyKey)) {
final cycleStartIndex = _cycleDetector!._resolutionHistory.indexOf(dependencyKey); final cycleStartIndex =
_cycleDetector!._resolutionHistory.indexOf(dependencyKey);
final cycle = _cycleDetector!._resolutionHistory.sublist(cycleStartIndex) final cycle = _cycleDetector!._resolutionHistory.sublist(cycleStartIndex)
..add(dependencyKey); ..add(dependencyKey);
observer.onCycleDetected(cycle); observer.onCycleDetected(cycle);

View File

@@ -14,7 +14,6 @@
import 'dart:collection'; import 'dart:collection';
import 'package:cherrypick/cherrypick.dart'; import 'package:cherrypick/cherrypick.dart';
/// GlobalCycleDetector detects and prevents circular dependencies across an entire DI scope hierarchy. /// GlobalCycleDetector detects and prevents circular dependencies across an entire DI scope hierarchy.
/// ///
/// This is particularly important for modular/feature-based applications /// This is particularly important for modular/feature-based applications
@@ -45,13 +44,16 @@ class GlobalCycleDetector {
final List<String> _globalResolutionHistory = []; final List<String> _globalResolutionHistory = [];
// Map of active detectors for subscopes (rarely used directly) // 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. /// Returns the singleton global detector instance, initializing it if needed.
static GlobalCycleDetector get instance { static GlobalCycleDetector get instance {
_instance ??= GlobalCycleDetector._internal(observer: CherryPick.globalObserver); _instance ??=
GlobalCycleDetector._internal(observer: CherryPick.globalObserver);
return _instance!; return _instance!;
} }
@@ -70,9 +72,11 @@ class GlobalCycleDetector {
if (_globalResolutionStack.contains(dependencyKey)) { if (_globalResolutionStack.contains(dependencyKey)) {
final cycleStartIndex = _globalResolutionHistory.indexOf(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.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( throw CircularDependencyException(
'Global circular dependency detected for $dependencyKey', 'Global circular dependency detected for $dependencyKey',
cycle, cycle,
@@ -88,7 +92,8 @@ class GlobalCycleDetector {
final dependencyKey = _createDependencyKeyFromType(T, named, scopeId); final dependencyKey = _createDependencyKeyFromType(T, named, scopeId);
_globalResolutionStack.remove(dependencyKey); _globalResolutionStack.remove(dependencyKey);
if (_globalResolutionHistory.isNotEmpty && _globalResolutionHistory.last == dependencyKey) { if (_globalResolutionHistory.isNotEmpty &&
_globalResolutionHistory.last == dependencyKey) {
_globalResolutionHistory.removeLast(); _globalResolutionHistory.removeLast();
} }
} }
@@ -101,13 +106,16 @@ class GlobalCycleDetector {
String? scopeId, String? scopeId,
T Function() action, T Function() action,
) { ) {
final dependencyKey = _createDependencyKeyFromType(dependencyType, named, scopeId); final dependencyKey =
_createDependencyKeyFromType(dependencyType, named, scopeId);
if (_globalResolutionStack.contains(dependencyKey)) { if (_globalResolutionStack.contains(dependencyKey)) {
final cycleStartIndex = _globalResolutionHistory.indexOf(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.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( throw CircularDependencyException(
'Global circular dependency detected for $dependencyKey', 'Global circular dependency detected for $dependencyKey',
cycle, cycle,
@@ -121,7 +129,8 @@ class GlobalCycleDetector {
return action(); return action();
} finally { } finally {
_globalResolutionStack.remove(dependencyKey); _globalResolutionStack.remove(dependencyKey);
if (_globalResolutionHistory.isNotEmpty && _globalResolutionHistory.last == dependencyKey) { if (_globalResolutionHistory.isNotEmpty &&
_globalResolutionHistory.last == dependencyKey) {
_globalResolutionHistory.removeLast(); _globalResolutionHistory.removeLast();
} }
} }
@@ -129,7 +138,8 @@ class GlobalCycleDetector {
/// Get per-scope detector (not usually needed by consumers). /// Get per-scope detector (not usually needed by consumers).
CycleDetector getScopeDetector(String scopeId) { 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. /// Remove detector for a given scope.
@@ -144,7 +154,8 @@ class GlobalCycleDetector {
} }
/// Get current global dependency resolution chain (for debugging or diagnostics). /// 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. /// Clears all global and per-scope state in this detector.
void clear() { void clear() {
@@ -157,7 +168,8 @@ class GlobalCycleDetector {
void _detectorClear(detector) => detector.clear(); void _detectorClear(detector) => detector.clear();
/// Creates a unique dependency key string including scope and name (for diagnostics/cycle checks). /// 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 typeName = type.toString();
final namePrefix = named != null ? '@$named' : ''; final namePrefix = named != null ? '@$named' : '';
final scopePrefix = scopeId != null ? '[$scopeId]' : ''; 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:cherrypick/src/observer.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
Scope? _rootScope; Scope? _rootScope;
/// Global logger for all [Scope]s managed by [CherryPick]. /// Global logger for all [Scope]s managed by [CherryPick].
@@ -80,7 +79,8 @@ class CherryPick {
if (_globalCycleDetectionEnabled && !_rootScope!.isCycleDetectionEnabled) { if (_globalCycleDetectionEnabled && !_rootScope!.isCycleDetectionEnabled) {
_rootScope!.enableCycleDetection(); _rootScope!.enableCycleDetection();
} }
if (_globalCrossScopeCycleDetectionEnabled && !_rootScope!.isGlobalCycleDetectionEnabled) { if (_globalCrossScopeCycleDetectionEnabled &&
!_rootScope!.isGlobalCycleDetectionEnabled) {
_rootScope!.enableGlobalCycleDetection(); _rootScope!.enableGlobalCycleDetection();
} }
return _rootScope!; return _rootScope!;
@@ -96,7 +96,8 @@ class CherryPick {
/// ``` /// ```
static Future<void> closeRootScope() async { static Future<void> closeRootScope() async {
if (_rootScope != null) { if (_rootScope != null) {
await _rootScope!.dispose(); // Автоматический вызов dispose для rootScope! await _rootScope!
.dispose(); // Автоматический вызов dispose для rootScope!
_rootScope = null; _rootScope = null;
} }
} }
@@ -141,13 +142,15 @@ class CherryPick {
/// ```dart /// ```dart
/// CherryPick.enableCycleDetectionForScope(scopeName: 'api.feature'); /// CherryPick.enableCycleDetectionForScope(scopeName: 'api.feature');
/// ``` /// ```
static void enableCycleDetectionForScope({String scopeName = '', String separator = '.'}) { static void enableCycleDetectionForScope(
{String scopeName = '', String separator = '.'}) {
final scope = _getScope(scopeName, separator); final scope = _getScope(scopeName, separator);
scope.enableCycleDetection(); scope.enableCycleDetection();
} }
/// Disables cycle detection for a given scope. See [enableCycleDetectionForScope]. /// 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); final scope = _getScope(scopeName, separator);
scope.disableCycleDetection(); scope.disableCycleDetection();
} }
@@ -158,7 +161,8 @@ class CherryPick {
/// ```dart /// ```dart
/// CherryPick.isCycleDetectionEnabledForScope(scopeName: 'feature.api'); /// CherryPick.isCycleDetectionEnabledForScope(scopeName: 'feature.api');
/// ``` /// ```
static bool isCycleDetectionEnabledForScope({String scopeName = '', String separator = '.'}) { static bool isCycleDetectionEnabledForScope(
{String scopeName = '', String separator = '.'}) {
final scope = _getScope(scopeName, separator); final scope = _getScope(scopeName, separator);
return scope.isCycleDetectionEnabled; return scope.isCycleDetectionEnabled;
} }
@@ -171,7 +175,8 @@ class CherryPick {
/// ```dart /// ```dart
/// print(CherryPick.getCurrentResolutionChain(scopeName: 'feature.api')); /// 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); final scope = _getScope(scopeName, separator);
return scope.currentResolutionChain; return scope.currentResolutionChain;
} }
@@ -229,14 +234,13 @@ class CherryPick {
if (nameParts.isEmpty) { if (nameParts.isEmpty) {
throw Exception('Can not open sub scope because scopeName can not split'); throw Exception('Can not open sub scope because scopeName can not split');
} }
final scope = nameParts.fold( final scope = nameParts.fold(openRootScope(),
openRootScope(), (Scope previous, String element) => previous.openSubScope(element));
(Scope previous, String element) => previous.openSubScope(element)
);
if (_globalCycleDetectionEnabled && !scope.isCycleDetectionEnabled) { if (_globalCycleDetectionEnabled && !scope.isCycleDetectionEnabled) {
scope.enableCycleDetection(); scope.enableCycleDetection();
} }
if (_globalCrossScopeCycleDetectionEnabled && !scope.isGlobalCycleDetectionEnabled) { if (_globalCrossScopeCycleDetectionEnabled &&
!scope.isGlobalCycleDetectionEnabled) {
scope.enableGlobalCycleDetection(); scope.enableGlobalCycleDetection();
} }
return scope; return scope;
@@ -252,21 +256,21 @@ class CherryPick {
/// CherryPick.closeScope(scopeName: 'network.super.api'); /// CherryPick.closeScope(scopeName: 'network.super.api');
/// ``` /// ```
@experimental @experimental
static Future<void> closeScope({String scopeName = '', String separator = '.'}) async { static Future<void> closeScope(
{String scopeName = '', String separator = '.'}) async {
if (scopeName.isEmpty) { if (scopeName.isEmpty) {
await closeRootScope(); await closeRootScope();
return; return;
} }
final nameParts = scopeName.split(separator); final nameParts = scopeName.split(separator);
if (nameParts.isEmpty) { 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) { if (nameParts.length > 1) {
final lastPart = nameParts.removeLast(); final lastPart = nameParts.removeLast();
final scope = nameParts.fold( final scope = nameParts.fold(openRootScope(),
openRootScope(), (Scope previous, String element) => previous.openSubScope(element));
(Scope previous, String element) => previous.openSubScope(element)
);
await scope.closeSubScope(lastPart); await scope.closeSubScope(lastPart);
} else { } else {
await openRootScope().closeSubScope(nameParts.first); await openRootScope().closeSubScope(nameParts.first);
@@ -316,7 +320,8 @@ class CherryPick {
/// print('Global cross-scope detection is ON'); /// 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). /// Returns the current global dependency resolution chain (across all scopes).
/// ///
@@ -367,7 +372,8 @@ class CherryPick {
/// ```dart /// ```dart
/// final featureScope = CherryPick.openGlobalSafeScope(scopeName: 'featureA.api'); /// 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); final scope = openScope(scopeName: scopeName, separator: separator);
scope.enableCycleDetection(); scope.enableCycleDetection();
scope.enableGlobalCycleDetection(); scope.enableGlobalCycleDetection();

View File

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

View File

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

View File

@@ -1,8 +1,8 @@
name: cherrypick name: cherrypick
description: Cherrypick is a small dependency injection (DI) library for dart/flutter projects. description: Cherrypick is a small dependency injection (DI) library for dart/flutter projects.
version: 3.0.0-dev.9 version: 3.0.0-dev.10
homepage: https://pese-git.github.io/cherrypick-site/ homepage: https://cherrypick-di.dev/
documentation: https://github.com/pese-git/cherrypick/wiki documentation: https://cherrypick-di.dev/docs/intro
repository: https://github.com/pese-git/cherrypick repository: https://github.com/pese-git/cherrypick
issue_tracker: https://github.com/pese-git/cherrypick/issues issue_tracker: https://github.com/pese-git/cherrypick/issues
topics: topics:

View File

@@ -12,6 +12,7 @@ class DummyModule extends Module {
} }
class A {} class A {}
class B {} class B {}
class CyclicModule extends Module { class CyclicModule extends Module {
@@ -52,10 +53,13 @@ void main() {
throwsA(isA<CircularDependencyException>()), throwsA(isA<CircularDependencyException>()),
); );
// Проверяем, что цикл зафиксирован либо в errors, либо в diagnostics либо cycles // Проверяем, что цикл зафиксирован либо в errors, либо в diagnostics либо cycles
final foundInErrors = observer.errors.any((m) => m.contains('cycle detected')); final foundInErrors =
final foundInDiagnostics = observer.diagnostics.any((m) => m.contains('cycle detected')); observer.errors.any((m) => m.contains('cycle detected'));
final foundInDiagnostics =
observer.diagnostics.any((m) => m.contains('cycle detected'));
final foundCycleNotified = observer.cycles.isNotEmpty; final foundCycleNotified = observer.cycles.isNotEmpty;
expect(foundInErrors || foundInDiagnostics || foundCycleNotified, isTrue, 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,8 +15,7 @@ class MockObserver implements CherryPickObserver {
void onWarning(String message, {Object? details}) => warnings.add(message); void onWarning(String message, {Object? details}) => warnings.add(message);
@override @override
void onError(String message, Object? error, StackTrace? stackTrace) => void onError(String message, Object? error, StackTrace? stackTrace) => errors.add(
errors.add(
'$message${error != null ? ' $error' : ''}${stackTrace != null ? '\n$stackTrace' : ''}'); '$message${error != null ? ' $error' : ''}${stackTrace != null ? '\n$stackTrace' : ''}');
@override @override
@@ -30,9 +29,11 @@ class MockObserver implements CherryPickObserver {
@override @override
void onInstanceRequested(String name, Type type, {String? scopeName}) {} void onInstanceRequested(String name, Type type, {String? scopeName}) {}
@override @override
void onInstanceCreated(String name, Type type, Object instance, {String? scopeName}) {} void onInstanceCreated(String name, Type type, Object instance,
{String? scopeName}) {}
@override @override
void onInstanceDisposed(String name, Type type, Object instance, {String? scopeName}) {} void onInstanceDisposed(String name, Type type, Object instance,
{String? scopeName}) {}
@override @override
void onModulesInstalled(List<String> moduleNames, {String? scopeName}) {} void onModulesInstalled(List<String> moduleNames, {String? scopeName}) {}
@override @override

View File

@@ -46,7 +46,9 @@ 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(); final parentScope = CherryPick.openRootScope();
parentScope.enableCycleDetection(); parentScope.enableCycleDetection();

View File

@@ -52,8 +52,7 @@ void main() {
throwsA(predicate((e) => throwsA(predicate((e) =>
e is CircularDependencyException && e is CircularDependencyException &&
e.dependencyChain.contains('String') && e.dependencyChain.contains('String') &&
e.dependencyChain.length > 1 e.dependencyChain.length > 1)),
)),
); );
}); });
@@ -161,14 +160,16 @@ class ServiceB {
class CircularModuleA extends Module { class CircularModuleA extends Module {
@override @override
void builder(Scope currentScope) { void builder(Scope currentScope) {
bind<ServiceA>().toProvide(() => ServiceA(currentScope.resolve<ServiceB>())); bind<ServiceA>()
.toProvide(() => ServiceA(currentScope.resolve<ServiceB>()));
} }
} }
class CircularModuleB extends Module { class CircularModuleB extends Module {
@override @override
void builder(Scope currentScope) { void builder(Scope currentScope) {
bind<ServiceB>().toProvide(() => ServiceB(currentScope.resolve<ServiceA>())); bind<ServiceB>()
.toProvide(() => ServiceB(currentScope.resolve<ServiceA>()));
} }
} }

View File

@@ -37,7 +37,9 @@ void main() {
expect(CherryPick.isGlobalCrossScopeCycleDetectionEnabled, isFalse); 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(); CherryPick.enableGlobalCrossScopeCycleDetection();
final scope = CherryPick.openRootScope(); final scope = CherryPick.openRootScope();
@@ -45,7 +47,9 @@ void main() {
expect(scope.isGlobalCycleDetectionEnabled, isTrue); 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(); final scope = CherryPick.openRootScope();
expect(scope.isGlobalCycleDetectionEnabled, isFalse); expect(scope.isGlobalCycleDetectionEnabled, isFalse);
@@ -56,15 +60,18 @@ void main() {
}); });
group('Global Safe Scope Creation', () { 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(); final scope = CherryPick.openGlobalSafeRootScope();
expect(scope.isCycleDetectionEnabled, isTrue); expect(scope.isCycleDetectionEnabled, isTrue);
expect(scope.isGlobalCycleDetectionEnabled, isTrue); expect(scope.isGlobalCycleDetectionEnabled, isTrue);
}); });
test('should create global safe sub-scope with both detections enabled', () { test('should create global safe sub-scope with both detections enabled',
final scope = CherryPick.openGlobalSafeScope(scopeName: 'feature.global'); () {
final scope =
CherryPick.openGlobalSafeScope(scopeName: 'feature.global');
expect(scope.isCycleDetectionEnabled, isTrue); expect(scope.isCycleDetectionEnabled, isTrue);
expect(scope.isGlobalCycleDetectionEnabled, isTrue); expect(scope.isGlobalCycleDetectionEnabled, isTrue);

View File

@@ -39,7 +39,9 @@ void main() {
expect(CherryPick.isGlobalCycleDetectionEnabled, isFalse); 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(); CherryPick.enableGlobalCycleDetection();
final scope = CherryPick.openRootScope(); final scope = CherryPick.openRootScope();
@@ -47,7 +49,9 @@ void main() {
expect(scope.isCycleDetectionEnabled, isTrue); 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(); final scope = CherryPick.openRootScope();
expect(scope.isCycleDetectionEnabled, isFalse); expect(scope.isCycleDetectionEnabled, isFalse);
@@ -56,7 +60,9 @@ void main() {
expect(scope.isCycleDetectionEnabled, isTrue); 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(); CherryPick.enableGlobalCycleDetection();
final scope = CherryPick.openRootScope(); final scope = CherryPick.openRootScope();
expect(scope.isCycleDetectionEnabled, isTrue); expect(scope.isCycleDetectionEnabled, isTrue);
@@ -99,21 +105,25 @@ void main() {
final scopeName = 'feature.auth'; final scopeName = 'feature.auth';
CherryPick.openScope(scopeName: scopeName); CherryPick.openScope(scopeName: scopeName);
expect(CherryPick.isCycleDetectionEnabledForScope(scopeName: scopeName), isFalse); expect(CherryPick.isCycleDetectionEnabledForScope(scopeName: scopeName),
isFalse);
CherryPick.enableCycleDetectionForScope(scopeName: scopeName); CherryPick.enableCycleDetectionForScope(scopeName: scopeName);
expect(CherryPick.isCycleDetectionEnabledForScope(scopeName: scopeName), isTrue); expect(CherryPick.isCycleDetectionEnabledForScope(scopeName: scopeName),
isTrue);
}); });
test('should disable cycle detection for specific scope', () { test('should disable cycle detection for specific scope', () {
final scopeName = 'feature.auth'; final scopeName = 'feature.auth';
CherryPick.enableCycleDetectionForScope(scopeName: scopeName); CherryPick.enableCycleDetectionForScope(scopeName: scopeName);
expect(CherryPick.isCycleDetectionEnabledForScope(scopeName: scopeName), isTrue); expect(CherryPick.isCycleDetectionEnabledForScope(scopeName: scopeName),
isTrue);
CherryPick.disableCycleDetectionForScope(scopeName: scopeName); CherryPick.disableCycleDetectionForScope(scopeName: scopeName);
expect(CherryPick.isCycleDetectionEnabledForScope(scopeName: scopeName), isFalse); expect(CherryPick.isCycleDetectionEnabledForScope(scopeName: scopeName),
isFalse);
}); });
}); });
@@ -134,14 +144,17 @@ void main() {
// Глобальная настройка отключена // Глобальная настройка отключена
expect(CherryPick.isGlobalCycleDetectionEnabled, isFalse); expect(CherryPick.isGlobalCycleDetectionEnabled, isFalse);
final scope = CherryPick.openSafeScope(scopeName: 'feature.independent'); final scope =
CherryPick.openSafeScope(scopeName: 'feature.independent');
expect(scope.isCycleDetectionEnabled, isTrue); expect(scope.isCycleDetectionEnabled, isTrue);
}); });
}); });
group('Resolution Chain Tracking', () { 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(); CherryPick.openRootScope();
final chain = CherryPick.getCurrentResolutionChain(); final chain = CherryPick.getCurrentResolutionChain();
@@ -149,7 +162,9 @@ void main() {
expect(chain, isEmpty); 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(); CherryPick.enableCycleDetectionForScope();
final chain = CherryPick.getCurrentResolutionChain(); final chain = CherryPick.getCurrentResolutionChain();
@@ -161,14 +176,17 @@ void main() {
final scopeName = 'feature.tracking'; final scopeName = 'feature.tracking';
CherryPick.enableCycleDetectionForScope(scopeName: scopeName); CherryPick.enableCycleDetectionForScope(scopeName: scopeName);
final chain = CherryPick.getCurrentResolutionChain(scopeName: scopeName); final chain =
CherryPick.getCurrentResolutionChain(scopeName: scopeName);
expect(chain, isEmpty); // Пустая, так как нет активного разрешения expect(chain, isEmpty); // Пустая, так как нет активного разрешения
}); });
}); });
group('Integration with Circular Dependencies', () { 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(); CherryPick.enableGlobalCycleDetection();
final scope = CherryPick.openRootScope(); final scope = CherryPick.openRootScope();
@@ -190,7 +208,9 @@ void main() {
); );
}); });
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(); final scope = CherryPick.openRootScope();
scope.installModules([CircularTestModule()]); scope.installModules([CircularTestModule()]);
@@ -205,7 +225,8 @@ void main() {
test('should handle empty scope name as root scope', () { test('should handle empty scope name as root scope', () {
CherryPick.enableCycleDetectionForScope(scopeName: ''); CherryPick.enableCycleDetectionForScope(scopeName: '');
expect(CherryPick.isCycleDetectionEnabledForScope(scopeName: ''), isTrue); expect(
CherryPick.isCycleDetectionEnabledForScope(scopeName: ''), isTrue);
expect(CherryPick.isCycleDetectionEnabledForScope(), isTrue); expect(CherryPick.isCycleDetectionEnabledForScope(), isTrue);
}); });
@@ -213,14 +234,21 @@ void main() {
final complexScopeName = 'app.feature.auth.login'; final complexScopeName = 'app.feature.auth.login';
CherryPick.enableCycleDetectionForScope(scopeName: complexScopeName); CherryPick.enableCycleDetectionForScope(scopeName: complexScopeName);
expect(CherryPick.isCycleDetectionEnabledForScope(scopeName: complexScopeName), isTrue); expect(
CherryPick.isCycleDetectionEnabledForScope(
scopeName: complexScopeName),
isTrue);
}); });
test('should handle custom separator', () { test('should handle custom separator', () {
final scopeName = 'app/feature/auth'; final scopeName = 'app/feature/auth';
CherryPick.enableCycleDetectionForScope(scopeName: scopeName, separator: '/'); CherryPick.enableCycleDetectionForScope(
scopeName: scopeName, separator: '/');
expect(CherryPick.isCycleDetectionEnabledForScope(scopeName: scopeName, separator: '/'), isTrue); expect(
CherryPick.isCycleDetectionEnabledForScope(
scopeName: scopeName, separator: '/'),
isTrue);
}); });
}); });
}); });
@@ -240,7 +268,9 @@ class CircularServiceB {
class CircularTestModule extends Module { class CircularTestModule extends Module {
@override @override
void builder(Scope currentScope) { void builder(Scope currentScope) {
bind<CircularServiceA>().toProvide(() => CircularServiceA(currentScope.resolve<CircularServiceB>())); bind<CircularServiceA>().toProvide(
bind<CircularServiceB>().toProvide(() => CircularServiceB(currentScope.resolve<CircularServiceA>())); () => 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 'dart:async';
import 'package:test/test.dart'; import 'package:test/test.dart';
import '../mock_logger.dart'; import '../mock_logger.dart';
@@ -18,7 +19,9 @@ class AsyncExampleDisposable implements Disposable {
class AsyncExampleModule extends Module { class AsyncExampleModule extends Module {
@override @override
void builder(Scope scope) { 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 { class ModuleCountingDisposable extends Module {
@override @override
void builder(Scope scope) { void builder(Scope scope) {
bind<CountingDisposable>().toProvide(() => CountingDisposable()).singleton(); bind<CountingDisposable>()
.toProvide(() => CountingDisposable())
.singleton();
} }
} }
@@ -99,8 +104,7 @@ class AsyncModule extends Module {
.toProvideAsync(() async { .toProvideAsync(() async {
await Future.delayed(Duration(milliseconds: 10)); await Future.delayed(Duration(milliseconds: 10));
return AsyncCreatedDisposable(); return AsyncCreatedDisposable();
}) }).singleton();
.singleton();
} }
} }
@@ -119,7 +123,8 @@ void main() {
final scope = Scope(null, observer: observer); final scope = Scope(null, observer: observer);
expect(Scope(scope, observer: observer), isNotNull); // эквивалент 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 observer = MockObserver();
final scope = Scope(null, observer: observer); final scope = Scope(null, observer: observer);
final subScope = scope.openSubScope("child"); final subScope = scope.openSubScope("child");
@@ -181,7 +186,8 @@ void main() {
}); });
test("After dropModules resolves fail", () { test("After dropModules resolves fail", () {
final observer = MockObserver(); 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); expect(scope.resolve<int>(), 5);
scope.dropModules(); scope.dropModules();
expect(() => scope.resolve<int>(), throwsA(isA<StateError>())); expect(() => scope.resolve<int>(), throwsA(isA<StateError>()));
@@ -294,7 +300,8 @@ void main() {
await scope.dispose(); await scope.dispose();
expect(t.disposed, isTrue); 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()); final scope = Scope(null, observer: MockObserver());
scope.installModules([ModuleWithDisposable()]); scope.installModules([ModuleWithDisposable()]);
final t1 = scope.resolve<TestDisposable>(); final t1 = scope.resolve<TestDisposable>();
@@ -305,7 +312,8 @@ void main() {
expect(t1.disposed, isTrue); expect(t1.disposed, isTrue);
expect(t2.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(); final scope = CherryPick.openRootScope();
scope.installModules([ModuleWithDisposable()]); scope.installModules([ModuleWithDisposable()]);
final t = scope.resolve<TestDisposable>(); final t = scope.resolve<TestDisposable>();
@@ -313,7 +321,8 @@ void main() {
await scope.dispose(); await scope.dispose();
expect(t.disposed, isTrue); 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(); final scope = CherryPick.openRootScope();
scope.installModules([ModuleWithDisposable()]); scope.installModules([ModuleWithDisposable()]);
final s = scope.resolve<String>(); final s = scope.resolve<String>();
@@ -327,7 +336,8 @@ void main() {
group('Scope/subScope dispose edge cases', () { group('Scope/subScope dispose edge cases', () {
test('Dispose called in closed subScope only', () async { test('Dispose called in closed subScope only', () async {
final root = CherryPick.openRootScope(); final root = CherryPick.openRootScope();
final sub = root.openSubScope('feature')..installModules([ModuleCountingDisposable()]); final sub = root.openSubScope('feature')
..installModules([ModuleCountingDisposable()]);
final d = sub.resolve<CountingDisposable>(); final d = sub.resolve<CountingDisposable>();
expect(d.disposeCount, 0); expect(d.disposeCount, 0);
@@ -339,7 +349,8 @@ void main() {
expect(d.disposeCount, 1); expect(d.disposeCount, 1);
// Повторное открытие subScope создает NEW instance (dispose на старый не вызовется снова) // Повторное открытие subScope создает NEW instance (dispose на старый не вызовется снова)
final sub2 = root.openSubScope('feature')..installModules([ModuleCountingDisposable()]); final sub2 = root.openSubScope('feature')
..installModules([ModuleCountingDisposable()]);
final d2 = sub2.resolve<CountingDisposable>(); final d2 = sub2.resolve<CountingDisposable>();
expect(identical(d, d2), isFalse); expect(identical(d, d2), isFalse);
await root.closeSubScope('feature'); await root.closeSubScope('feature');
@@ -347,8 +358,14 @@ void main() {
}); });
test('Dispose for all nested subScopes on root disposeAsync', () async { test('Dispose for all nested subScopes on root disposeAsync', () async {
final root = CherryPick.openRootScope(); final root = CherryPick.openRootScope();
root.openSubScope('a').openSubScope('b').installModules([ModuleCountingDisposable()]); root
final d = root.openSubScope('a').openSubScope('b').resolve<CountingDisposable>(); .openSubScope('a')
.openSubScope('b')
.installModules([ModuleCountingDisposable()]);
final d = root
.openSubScope('a')
.openSubScope('b')
.resolve<CountingDisposable>();
await root.dispose(); await root.dispose();
expect(d.disposeCount, 1); expect(d.disposeCount, 1);
}); });
@@ -357,7 +374,8 @@ void main() {
// -------------------------------------------------------------------------- // --------------------------------------------------------------------------
group('Async disposable (Future test)', () { group('Async disposable (Future test)', () {
test('Async Disposable is awaited on disposeAsync', () async { 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>(); final d = scope.resolve<AsyncExampleDisposable>();
expect(d.disposed, false); expect(d.disposed, false);
await scope.dispose(); await scope.dispose();

View File

@@ -1,3 +1,7 @@
## 1.1.2-dev.1
- **DOCS**(pub): update homepage and documentation URLs in pubspec.yaml to new official site.
## 1.1.2-dev.0 ## 1.1.2-dev.0
- **DOCS**(annotations): unify and improve English DartDoc for all DI annotations. - **DOCS**(annotations): unify and improve English DartDoc for all DI annotations.

View File

@@ -1,8 +1,9 @@
name: cherrypick_annotations name: cherrypick_annotations
description: | description: |
Set of annotations for CherryPick dependency injection library. Enables code generation and declarative DI for Dart & Flutter projects. Set of annotations for CherryPick dependency injection library. Enables code generation and declarative DI for Dart & Flutter projects.
version: 1.1.2-dev.0 version: 1.1.2-dev.1
documentation: https://github.com/pese-git/cherrypick/wiki homepage: https://cherrypick-di.dev/
documentation: https://cherrypick-di.dev/docs/intro
repository: https://github.com/pese-git/cherrypick/cherrypick_annotations repository: https://github.com/pese-git/cherrypick/cherrypick_annotations
issue_tracker: https://github.com/pese-git/cherrypick/issues issue_tracker: https://github.com/pese-git/cherrypick/issues
topics: topics:

View File

@@ -1,3 +1,7 @@
## 1.1.3-dev.10
- **DOCS**(pub): update homepage and documentation URLs in pubspec.yaml to new official site.
## 1.1.3-dev.9 ## 1.1.3-dev.9
- **DOCS**(provider): add detailed English API documentation for CherryPickProvider Flutter integration. - **DOCS**(provider): add detailed English API documentation for CherryPickProvider Flutter integration.

View File

@@ -1,8 +1,8 @@
name: cherrypick_flutter name: cherrypick_flutter
description: "Flutter library that allows access to the root scope through the context using `CherryPickProvider`." description: "Flutter library that allows access to the root scope through the context using `CherryPickProvider`."
version: 1.1.3-dev.9 version: 1.1.3-dev.10
homepage: https://pese-git.github.io/cherrypick-site/ homepage: https://cherrypick-di.dev/
documentation: https://github.com/pese-git/cherrypick/wiki documentation: https://cherrypick-di.dev/docs/intro
repository: https://github.com/pese-git/cherrypick repository: https://github.com/pese-git/cherrypick
issue_tracker: https://github.com/pese-git/cherrypick/issues issue_tracker: https://github.com/pese-git/cherrypick/issues
topics: topics:
@@ -19,7 +19,7 @@ environment:
dependencies: dependencies:
flutter: flutter:
sdk: flutter sdk: flutter
cherrypick: ^3.0.0-dev.9 cherrypick: ^3.0.0-dev.10
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:

View File

@@ -1,3 +1,7 @@
## 2.0.0-dev.1
- **DOCS**(pub): update homepage and documentation URLs in pubspec.yaml to new official site.
## 2.0.0-dev.0 ## 2.0.0-dev.0
> Note: This release has breaking changes. > Note: This release has breaking changes.

View File

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

View File

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

View File

@@ -2,8 +2,9 @@ name: cherrypick_generator
description: | description: |
Source code generator for the cherrypick dependency injection system. Processes annotations to generate binding and module code for Dart & Flutter projects. Source code generator for the cherrypick dependency injection system. Processes annotations to generate binding and module code for Dart & Flutter projects.
version: 2.0.0-dev.0 version: 2.0.0-dev.1
documentation: https://github.com/pese-git/cherrypick/wiki homepage: https://cherrypick-di.dev/
documentation: https://cherrypick-di.dev/docs/intro
repository: https://github.com/pese-git/cherrypick/cherrypick_generator repository: https://github.com/pese-git/cherrypick/cherrypick_generator
issue_tracker: https://github.com/pese-git/cherrypick/issues issue_tracker: https://github.com/pese-git/cherrypick/issues
topics: topics:
@@ -18,7 +19,7 @@ environment:
# Add regular dependencies here. # Add regular dependencies here.
dependencies: dependencies:
cherrypick_annotations: ^1.1.2-dev.0 cherrypick_annotations: ^1.1.2-dev.1
analyzer: ^7.0.0 analyzer: ^7.0.0
dart_style: ^3.0.0 dart_style: ^3.0.0
build: ^2.4.1 build: ^2.4.1

View File

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

View File

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

View File

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

View File

@@ -22,7 +22,9 @@ abstract class AppModule extends Module {
@provide() @provide()
@singleton() @singleton()
TalkerDioLogger talkerDioLogger(Talker talker, TalkerDioLoggerSettings settings) => TalkerDioLogger(talker: talker, settings: settings); TalkerDioLogger talkerDioLogger(
Talker talker, TalkerDioLoggerSettings settings) =>
TalkerDioLogger(talker: talker, settings: settings);
@instance() @instance()
int timeout() => 1000; int timeout() => 1000;

View File

@@ -13,7 +13,6 @@ void main() {
final talker = Talker(); final talker = Talker();
final talkerLogger = TalkerCherryPickObserver(talker); final talkerLogger = TalkerCherryPickObserver(talker);
Bloc.observer = TalkerBlocObserver(talker: talker); Bloc.observer = TalkerBlocObserver(talker: talker);
CherryPick.setGlobalObserver(talkerLogger); CherryPick.setGlobalObserver(talkerLogger);
@@ -24,7 +23,10 @@ void main() {
} }
// Используем safe root scope для гарантии защиты // Используем 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" path: "../../cherrypick"
relative: true relative: true
source: path source: path
version: "3.0.0-dev.8" version: "3.0.0-dev.9"
cherrypick_annotations: cherrypick_annotations:
dependency: "direct main" dependency: "direct main"
description: description:
path: "../../cherrypick_annotations" path: "../../cherrypick_annotations"
relative: true relative: true
source: path source: path
version: "1.1.1" version: "1.1.2-dev.0"
cherrypick_generator: cherrypick_generator:
dependency: "direct main" dependency: "direct main"
description: description:
path: "../../cherrypick_generator" path: "../../cherrypick_generator"
relative: true relative: true
source: path source: path
version: "1.1.1" version: "2.0.0-dev.0"
cli_launcher: cli_launcher:
dependency: transitive dependency: transitive
description: description:
@@ -864,7 +864,7 @@ packages:
path: "../../talker_cherrypick_logger" path: "../../talker_cherrypick_logger"
relative: true relative: true
source: path source: path
version: "1.0.0" version: "1.1.0-dev.3"
talker_dio_logger: talker_dio_logger:
dependency: "direct main" dependency: "direct main"
description: description:

View File

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

View File

@@ -1,7 +1,26 @@
# https://dart.dev/guides/libraries/private-files # See https://www.dartlang.org/guides/libraries/private-files
# Created by `dart pub`
.dart_tool/
# Avoid committing pubspec.lock for library packages; see # Files and directories created by pub
# https://dart.dev/guides/libraries/private-files#pubspeclock. .dart_tool/
.packages
build/
# If you're building an application, you may want to check-in your pubspec.lock
pubspec.lock pubspec.lock
# Directory created by dartdoc
# If you don't generate documentation locally you can remove this line.
doc/api/
# Avoid committing generated Javascript files:
*.dart.js
*.info.json # Produced by the --dump-info flag.
*.js # When generated by dart2js. Don't specify *.js if your
# project includes source files written in JavaScript.
*.js_
*.js.deps
*.js.map
# FVM Version Cache
.fvm/
pubspec_overrides.yaml

View File

@@ -1,3 +1,17 @@
## 1.1.0-dev.5
- **DOCS**(pub): update homepage and documentation URLs in pubspec.yaml to new official site.
## 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
- Bump "talker_cherrypick_logger" to `1.1.0-dev.2`.
## 1.1.0-dev.0 ## 1.1.0-dev.0
- **FEAT**(logging): add talker_dio_logger and talker_bloc_logger integration, improve cherrypick logger structure, add UI log screen for DI and network/bloc debug. - **FEAT**(logging): add talker_dio_logger and talker_bloc_logger integration, improve cherrypick logger structure, add UI log screen for DI and network/bloc debug.

View File

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

View File

@@ -69,26 +69,32 @@ class TalkerCherryPickObserver implements CherryPickObserver {
/// Called when a new instance is created. /// Called when a new instance is created.
@override @override
void onInstanceCreated(String name, Type type, Object instance, {String? scopeName}) { void onInstanceCreated(String name, Type type, Object instance,
talker.info('[create][CherryPick] $name$type => $instance (scope: $scopeName)'); {String? scopeName}) {
talker.info(
'[create][CherryPick] $name$type => $instance (scope: $scopeName)');
} }
/// Called when an instance is disposed. /// Called when an instance is disposed.
@override @override
void onInstanceDisposed(String name, Type type, Object instance, {String? scopeName}) { void onInstanceDisposed(String name, Type type, Object instance,
talker.info('[dispose][CherryPick] $name$type => $instance (scope: $scopeName)'); {String? scopeName}) {
talker.info(
'[dispose][CherryPick] $name$type => $instance (scope: $scopeName)');
} }
/// Called when modules are installed. /// Called when modules are installed.
@override @override
void onModulesInstalled(List<String> modules, {String? scopeName}) { 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. /// Called when modules are removed.
@override @override
void onModulesRemoved(List<String> modules, {String? scopeName}) { 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. /// 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. /// Called if the DI container detects a cycle in the dependency graph.
@override @override
void onCycleDetected(List<String> chain, {String? scopeName}) { 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. /// 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. /// Called for error events with optional stack trace.
@override @override
void onError(String message, Object? error, StackTrace? stackTrace) { 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,8 +1,8 @@
name: talker_cherrypick_logger name: talker_cherrypick_logger
description: A starting point for Dart libraries or applications. description: A Talker logger integration for CherryPick DI to observe and log DI events and errors.
version: 1.1.0-dev.0 version: 1.1.0-dev.5
homepage: https://pese-git.github.io/cherrypick-site/ homepage: https://cherrypick-di.dev/
documentation: https://github.com/pese-git/cherrypick/wiki documentation: https://cherrypick-di.dev/docs/intro
repository: https://github.com/pese-git/cherrypick repository: https://github.com/pese-git/cherrypick
issue_tracker: https://github.com/pese-git/cherrypick/issues issue_tracker: https://github.com/pese-git/cherrypick/issues
@@ -18,7 +18,7 @@ environment:
# Add regular dependencies here. # Add regular dependencies here.
dependencies: dependencies:
talker: ^4.9.3 talker: ^4.9.3
cherrypick: ^3.0.0-dev.9 cherrypick: ^3.0.0-dev.10
dev_dependencies: dev_dependencies:

View File

@@ -15,7 +15,8 @@ void main() {
test('onInstanceRequested logs info', () { test('onInstanceRequested logs info', () {
observer.onInstanceRequested('A', String, scopeName: 'test'); observer.onInstanceRequested('A', String, scopeName: 'test');
final log = talker.history.last; 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', () { test('onCycleDetected logs warning', () {