diff --git a/.access b/.access new file mode 100644 index 0000000..95e0f96 --- /dev/null +++ b/.access @@ -0,0 +1,9 @@ +main server: +ssh root@147.45.152.129 + +system: +root:jH^W5qHSh,zCQp +vscode:AGktOidxrah1KVC0 + +forgejo account: +cinnabarflower:uS1jOollFpPv82iYcOiv7YHEG28d diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..e69de29 diff --git a/mnemo_cards_backend/lib/main.dart b/mnemo_cards_backend/lib/main.dart index 249c713..a1df905 100644 --- a/mnemo_cards_backend/lib/main.dart +++ b/mnemo_cards_backend/lib/main.dart @@ -67,6 +67,7 @@ void main(List args) async { name: 'db', inspector: args.contains('--debug'), ); + getIt.registerSingleton(isar); configureDependencies(); await getIt().initV2(args); diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/.github/workflows/yx_scope_pr.yaml b/mnemo_cards_web_v2/packages/yx/city-services-pub/.github/workflows/yx_scope_pr.yaml new file mode 100644 index 0000000..cd21034 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/.github/workflows/yx_scope_pr.yaml @@ -0,0 +1,55 @@ +name: CI for PRs + +on: + pull_request: + paths: + - "**/yx_scope/**" + - "**/.github/**" + - "!**.md" + branches: + - main + +jobs: + ci_checks: + name: CI Checks + runs-on: ubuntu-latest + + strategy: + matrix: + # Check the library against the most recent version and the previous minor version + flutter-version: [3.32.4, 3.29.2, 3.27.1, 3.24.5] + package: + - yx_scope/packages/yx_scope + - yx_scope/packages/yx_scope_flutter + - yx_scope/packages/yx_scope_linter + + defaults: + run: + working-directory: ${{ matrix.package }} + shell: bash + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Flutter + uses: subosito/flutter-action@v2 + with: + flutter-version: ${{ matrix.flutter-version }} + + - name: Install dependencies + run: flutter pub get + + - name: Check formatting + run: dart format --set-exit-if-changed . + + - name: Run linter + run: flutter analyze + + - name: Verify custom_lint + working-directory: yx_scope/packages/yx_scope_linter/example + run: dart run custom_lint --watch + + - name: Run tests + if: matrix.package != 'yx_scope/packages/yx_scope_linter' + run: flutter test diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/.mapping.json b/mnemo_cards_web_v2/packages/yx/city-services-pub/.mapping.json new file mode 100644 index 0000000..10e5ab6 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/.mapping.json @@ -0,0 +1,254 @@ +{ + "":"flutter/pro/opensource", + ".":"flutter/pro/opensource", + ".github/workflows/yx_scope_pr.yaml":"flutter/pro/opensource/.github/workflows/yx_scope_pr.yaml", + "CONTRIBUTING.md":"flutter/pro/opensource/CONTRIBUTING.md", + "README.md":"flutter/pro/opensource/README.md", + "yx_scope/.gitignore":"flutter/pro/opensource/yx_scope/.gitignore", + "yx_scope/AUTHORS":"flutter/pro/opensource/yx_scope/AUTHORS", + "yx_scope/CONTRIBUTING.md":"flutter/pro/opensource/yx_scope/CONTRIBUTING.md", + "yx_scope/LICENSE":"flutter/pro/opensource/yx_scope/LICENSE", + "yx_scope/README.md":"flutter/pro/opensource/yx_scope/README.md", + "yx_scope/assets/logos/yx_scope.webp":"flutter/pro/opensource/yx_scope/assets/logos/yx_scope.webp", + "yx_scope/packages/yx_scope/.gitignore":"flutter/pro/opensource/yx_scope/packages/yx_scope/.gitignore", + "yx_scope/packages/yx_scope/.metadata":"flutter/pro/opensource/yx_scope/packages/yx_scope/.metadata", + "yx_scope/packages/yx_scope/CHANGELOG.md":"flutter/pro/opensource/yx_scope/packages/yx_scope/CHANGELOG.md", + "yx_scope/packages/yx_scope/LICENSE":"flutter/pro/opensource/yx_scope/packages/yx_scope/LICENSE", + "yx_scope/packages/yx_scope/README.md":"flutter/pro/opensource/yx_scope/packages/yx_scope/README.md", + "yx_scope/packages/yx_scope/analysis_options.yaml":"flutter/pro/opensource/yx_scope/packages/yx_scope/analysis_options.yaml", + "yx_scope/packages/yx_scope/doc/assets/scope_anatomy.png":"flutter/pro/opensource/yx_scope/packages/yx_scope/doc/assets/scope_anatomy.png", + "yx_scope/packages/yx_scope/doc/live_templates.md":"flutter/pro/opensource/yx_scope/packages/yx_scope/doc/live_templates.md", + "yx_scope/packages/yx_scope/example/.gitignore":"flutter/pro/opensource/yx_scope/packages/yx_scope/example/.gitignore", + "yx_scope/packages/yx_scope/example/.metadata":"flutter/pro/opensource/yx_scope/packages/yx_scope/example/.metadata", + "yx_scope/packages/yx_scope/example/CHANGELOG.md":"flutter/pro/opensource/yx_scope/packages/yx_scope/example/CHANGELOG.md", + "yx_scope/packages/yx_scope/example/LICENSE":"flutter/pro/opensource/yx_scope/packages/yx_scope/example/LICENSE", + "yx_scope/packages/yx_scope/example/README.md":"flutter/pro/opensource/yx_scope/packages/yx_scope/example/README.md", + "yx_scope/packages/yx_scope/example/analysis_options.yaml":"flutter/pro/opensource/yx_scope/packages/yx_scope/example/analysis_options.yaml", + "yx_scope/packages/yx_scope/example/bin/app_listener.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope/example/bin/app_listener.dart", + "yx_scope/packages/yx_scope/example/bin/deprecated_listeners_main.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope/example/bin/deprecated_listeners_main.dart", + "yx_scope/packages/yx_scope/example/bin/main.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope/example/bin/main.dart", + "yx_scope/packages/yx_scope/example/pubspec.yaml":"flutter/pro/opensource/yx_scope/packages/yx_scope/example/pubspec.yaml", + "yx_scope/packages/yx_scope/lib/advanced.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope/lib/advanced.dart", + "yx_scope/packages/yx_scope/lib/src/base_scope_container.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope/lib/src/base_scope_container.dart", + "yx_scope/packages/yx_scope/lib/src/core/async_lifecycle.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope/lib/src/core/async_lifecycle.dart", + "yx_scope/packages/yx_scope/lib/src/core/scope_exception.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope/lib/src/core/scope_exception.dart", + "yx_scope/packages/yx_scope/lib/src/core/scope_state.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope/lib/src/core/scope_state.dart", + "yx_scope/packages/yx_scope/lib/src/core/scope_state_holder.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope/lib/src/core/scope_state_holder.dart", + "yx_scope/packages/yx_scope/lib/src/core_scope_holder.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope/lib/src/core_scope_holder.dart", + "yx_scope/packages/yx_scope/lib/src/custom_dep.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope/lib/src/custom_dep.dart", + "yx_scope/packages/yx_scope/lib/src/dep.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope/lib/src/dep.dart", + "yx_scope/packages/yx_scope/lib/src/monitoring/listeners.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope/lib/src/monitoring/listeners.dart", + "yx_scope/packages/yx_scope/lib/src/monitoring/models/dep_id.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope/lib/src/monitoring/models/dep_id.dart", + "yx_scope/packages/yx_scope/lib/src/monitoring/models/scope_id.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope/lib/src/monitoring/models/scope_id.dart", + "yx_scope/packages/yx_scope/lib/src/monitoring/models/scope_meta.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope/lib/src/monitoring/models/scope_meta.dart", + "yx_scope/packages/yx_scope/lib/src/monitoring/models/value_meta.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope/lib/src/monitoring/models/value_meta.dart", + "yx_scope/packages/yx_scope/lib/src/monitoring/observers.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope/lib/src/monitoring/observers.dart", + "yx_scope/packages/yx_scope/lib/src/monitoring/observers_internal.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope/lib/src/monitoring/observers_internal.dart", + "yx_scope/packages/yx_scope/lib/src/monitoring/raw_listeners.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope/lib/src/monitoring/raw_listeners.dart", + "yx_scope/packages/yx_scope/lib/src/monitoring/raw_observers.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope/lib/src/monitoring/raw_observers.dart", + "yx_scope/packages/yx_scope/lib/src/monitoring/scope_observatory.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope/lib/src/monitoring/scope_observatory.dart", + "yx_scope/packages/yx_scope/lib/src/monitoring/scope_observatory_internal.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope/lib/src/monitoring/scope_observatory_internal.dart", + "yx_scope/packages/yx_scope/lib/src/scope_container.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope/lib/src/scope_container.dart", + "yx_scope/packages/yx_scope/lib/src/scope_holder.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope/lib/src/scope_holder.dart", + "yx_scope/packages/yx_scope/lib/src/scope_module.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope/lib/src/scope_module.dart", + "yx_scope/packages/yx_scope/lib/src/scope_state_streamable.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope/lib/src/scope_state_streamable.dart", + "yx_scope/packages/yx_scope/lib/src/test_utils/scope_container_test_utils.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope/lib/src/test_utils/scope_container_test_utils.dart", + "yx_scope/packages/yx_scope/lib/src/test_utils/scope_state_test_utils.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope/lib/src/test_utils/scope_state_test_utils.dart", + "yx_scope/packages/yx_scope/lib/yx_scope.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope/lib/yx_scope.dart", + "yx_scope/packages/yx_scope/pubspec.yaml":"flutter/pro/opensource/yx_scope/packages/yx_scope/pubspec.yaml", + "yx_scope/packages/yx_scope/screenshots/yx_scope_logo.webp":"flutter/pro/opensource/yx_scope/packages/yx_scope/screenshots/yx_scope_logo.webp", + "yx_scope/packages/yx_scope/test/dep_test.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope/test/dep_test.dart", + "yx_scope/packages/yx_scope/test/deprecated_listeners_test.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope/test/deprecated_listeners_test.dart", + "yx_scope/packages/yx_scope/test/observers_test.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope/test/observers_test.dart", + "yx_scope/packages/yx_scope/test/parent_scope_test.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope/test/parent_scope_test.dart", + "yx_scope/packages/yx_scope/test/scope_state_streamable_test.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope/test/scope_state_streamable_test.dart", + "yx_scope/packages/yx_scope/test/scope_state_test.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope/test/scope_state_test.dart", + "yx_scope/packages/yx_scope/test/scope_test.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope/test/scope_test.dart", + "yx_scope/packages/yx_scope/test/utils/test_logger.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope/test/utils/test_logger.dart", + "yx_scope/packages/yx_scope/test/utils/utils.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope/test/utils/utils.dart", + "yx_scope/packages/yx_scope_flutter/.gitignore":"flutter/pro/opensource/yx_scope/packages/yx_scope_flutter/.gitignore", + "yx_scope/packages/yx_scope_flutter/.metadata":"flutter/pro/opensource/yx_scope/packages/yx_scope_flutter/.metadata", + "yx_scope/packages/yx_scope_flutter/CHANGELOG.md":"flutter/pro/opensource/yx_scope/packages/yx_scope_flutter/CHANGELOG.md", + "yx_scope/packages/yx_scope_flutter/LICENSE":"flutter/pro/opensource/yx_scope/packages/yx_scope_flutter/LICENSE", + "yx_scope/packages/yx_scope_flutter/README.md":"flutter/pro/opensource/yx_scope/packages/yx_scope_flutter/README.md", + "yx_scope/packages/yx_scope_flutter/analysis_options.yaml":"flutter/pro/opensource/yx_scope/packages/yx_scope_flutter/analysis_options.yaml", + "yx_scope/packages/yx_scope_flutter/doc/assets/scope_anatomy.png":"flutter/pro/opensource/yx_scope/packages/yx_scope_flutter/doc/assets/scope_anatomy.png", + "yx_scope/packages/yx_scope_flutter/doc/assets/scope_tree.png":"flutter/pro/opensource/yx_scope/packages/yx_scope_flutter/doc/assets/scope_tree.png", + "yx_scope/packages/yx_scope_flutter/doc/assets/scope_types.png":"flutter/pro/opensource/yx_scope/packages/yx_scope_flutter/doc/assets/scope_types.png", + "yx_scope/packages/yx_scope_flutter/doc/introduction.md":"flutter/pro/opensource/yx_scope/packages/yx_scope_flutter/doc/introduction.md", + "yx_scope/packages/yx_scope_flutter/example/.gitignore":"flutter/pro/opensource/yx_scope/packages/yx_scope_flutter/example/.gitignore", + "yx_scope/packages/yx_scope_flutter/example/.metadata":"flutter/pro/opensource/yx_scope/packages/yx_scope_flutter/example/.metadata", + "yx_scope/packages/yx_scope_flutter/example/README.md":"flutter/pro/opensource/yx_scope/packages/yx_scope_flutter/example/README.md", + "yx_scope/packages/yx_scope_flutter/example/analysis_options.yaml":"flutter/pro/opensource/yx_scope/packages/yx_scope_flutter/example/analysis_options.yaml", + "yx_scope/packages/yx_scope_flutter/example/lib/data/map/map.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope_flutter/example/lib/data/map/map.dart", + "yx_scope/packages/yx_scope_flutter/example/lib/data/map/map_widget.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope_flutter/example/lib/data/map/map_widget.dart", + "yx_scope/packages/yx_scope_flutter/example/lib/data/orders/incoming_orders_provider.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope_flutter/example/lib/data/orders/incoming_orders_provider.dart", + "yx_scope/packages/yx_scope_flutter/example/lib/data/orders/models/incoming_order_data.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope_flutter/example/lib/data/orders/models/incoming_order_data.dart", + "yx_scope/packages/yx_scope_flutter/example/lib/di/account/account_scope.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope_flutter/example/lib/di/account/account_scope.dart", + "yx_scope/packages/yx_scope_flutter/example/lib/di/app/app_scope.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope_flutter/example/lib/di/app/app_scope.dart", + "yx_scope/packages/yx_scope_flutter/example/lib/di/map/map_scope.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope_flutter/example/lib/di/map/map_scope.dart", + "yx_scope/packages/yx_scope_flutter/example/lib/di/map_navigation/map_navigation_scope.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope_flutter/example/lib/di/map_navigation/map_navigation_scope.dart", + "yx_scope/packages/yx_scope_flutter/example/lib/di/online_order/online_scope.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope_flutter/example/lib/di/online_order/online_scope.dart", + "yx_scope/packages/yx_scope_flutter/example/lib/di/order/order_scope.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope_flutter/example/lib/di/order/order_scope.dart", + "yx_scope/packages/yx_scope_flutter/example/lib/di/register/register_scope.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope_flutter/example/lib/di/register/register_scope.dart", + "yx_scope/packages/yx_scope_flutter/example/lib/di/utils/listeners.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope_flutter/example/lib/di/utils/listeners.dart", + "yx_scope/packages/yx_scope_flutter/example/lib/domain/auth/account_holder.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope_flutter/example/lib/domain/auth/account_holder.dart", + "yx_scope/packages/yx_scope_flutter/example/lib/domain/auth/auth_manager.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope_flutter/example/lib/domain/auth/auth_manager.dart", + "yx_scope/packages/yx_scope_flutter/example/lib/domain/auth/models/account.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope_flutter/example/lib/domain/auth/models/account.dart", + "yx_scope/packages/yx_scope_flutter/example/lib/domain/auth/models/account_params.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope_flutter/example/lib/domain/auth/models/account_params.dart", + "yx_scope/packages/yx_scope_flutter/example/lib/domain/auth/register_manager.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope_flutter/example/lib/domain/auth/register_manager.dart", + "yx_scope/packages/yx_scope_flutter/example/lib/domain/auth/register_state_holder.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope_flutter/example/lib/domain/auth/register_state_holder.dart", + "yx_scope/packages/yx_scope_flutter/example/lib/domain/map/map_factory.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope_flutter/example/lib/domain/map/map_factory.dart", + "yx_scope/packages/yx_scope_flutter/example/lib/domain/map/map_manager.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope_flutter/example/lib/domain/map/map_manager.dart", + "yx_scope/packages/yx_scope_flutter/example/lib/domain/map_navigation/map_navigation_manager.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope_flutter/example/lib/domain/map_navigation/map_navigation_manager.dart", + "yx_scope/packages/yx_scope_flutter/example/lib/domain/order/accept_order_manager.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope_flutter/example/lib/domain/order/accept_order_manager.dart", + "yx_scope/packages/yx_scope_flutter/example/lib/domain/order/models/order.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope_flutter/example/lib/domain/order/models/order.dart", + "yx_scope/packages/yx_scope_flutter/example/lib/domain/order/online_order_state_holder.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope_flutter/example/lib/domain/order/online_order_state_holder.dart", + "yx_scope/packages/yx_scope_flutter/example/lib/domain/order/order_manager.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope_flutter/example/lib/domain/order/order_manager.dart", + "yx_scope/packages/yx_scope_flutter/example/lib/domain/order/order_position_manager.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope_flutter/example/lib/domain/order/order_position_manager.dart", + "yx_scope/packages/yx_scope_flutter/example/lib/domain/order/orders_state_holder.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope_flutter/example/lib/domain/order/orders_state_holder.dart", + "yx_scope/packages/yx_scope_flutter/example/lib/domain/order_navigation/order_navigation_delegate.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope_flutter/example/lib/domain/order_navigation/order_navigation_delegate.dart", + "yx_scope/packages/yx_scope_flutter/example/lib/main.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope_flutter/example/lib/main.dart", + "yx_scope/packages/yx_scope_flutter/example/lib/router/app_state_observer.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope_flutter/example/lib/router/app_state_observer.dart", + "yx_scope/packages/yx_scope_flutter/example/lib/router/models/app_state.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope_flutter/example/lib/router/models/app_state.dart", + "yx_scope/packages/yx_scope_flutter/example/lib/router/router_delegate.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope_flutter/example/lib/router/router_delegate.dart", + "yx_scope/packages/yx_scope_flutter/example/lib/ui/account/account_page.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope_flutter/example/lib/ui/account/account_page.dart", + "yx_scope/packages/yx_scope_flutter/example/lib/ui/auth/auth_page.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope_flutter/example/lib/ui/auth/auth_page.dart", + "yx_scope/packages/yx_scope_flutter/example/lib/ui/map/map_page.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope_flutter/example/lib/ui/map/map_page.dart", + "yx_scope/packages/yx_scope_flutter/example/lib/ui/orders/accept_order_wrapper.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope_flutter/example/lib/ui/orders/accept_order_wrapper.dart", + "yx_scope/packages/yx_scope_flutter/example/lib/ui/orders/orders_page.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope_flutter/example/lib/ui/orders/orders_page.dart", + "yx_scope/packages/yx_scope_flutter/example/lib/ui/register/register_page.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope_flutter/example/lib/ui/register/register_page.dart", + "yx_scope/packages/yx_scope_flutter/example/lib/ui/tabbar/tabbar_page.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope_flutter/example/lib/ui/tabbar/tabbar_page.dart", + "yx_scope/packages/yx_scope_flutter/example/lib/utils/logger.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope_flutter/example/lib/utils/logger.dart", + "yx_scope/packages/yx_scope_flutter/example/pubspec.yaml":"flutter/pro/opensource/yx_scope/packages/yx_scope_flutter/example/pubspec.yaml", + "yx_scope/packages/yx_scope_flutter/example/pubspec_overrides.yaml":"flutter/pro/opensource/yx_scope/packages/yx_scope_flutter/example/pubspec_overrides.yaml", + "yx_scope/packages/yx_scope_flutter/lib/src/core/provider.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope_flutter/lib/src/core/provider.dart", + "yx_scope/packages/yx_scope_flutter/lib/src/core/scope_error.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope_flutter/lib/src/core/scope_error.dart", + "yx_scope/packages/yx_scope_flutter/lib/src/scope_builder.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope_flutter/lib/src/scope_builder.dart", + "yx_scope/packages/yx_scope_flutter/lib/src/scope_consumer.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope_flutter/lib/src/scope_consumer.dart", + "yx_scope/packages/yx_scope_flutter/lib/src/scope_listener.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope_flutter/lib/src/scope_listener.dart", + "yx_scope/packages/yx_scope_flutter/lib/src/scope_provider.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope_flutter/lib/src/scope_provider.dart", + "yx_scope/packages/yx_scope_flutter/lib/src/scope_widget_builder.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope_flutter/lib/src/scope_widget_builder.dart", + "yx_scope/packages/yx_scope_flutter/lib/src/scope_widget_listener.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope_flutter/lib/src/scope_widget_listener.dart", + "yx_scope/packages/yx_scope_flutter/lib/yx_scope_flutter.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope_flutter/lib/yx_scope_flutter.dart", + "yx_scope/packages/yx_scope_flutter/pubspec.yaml":"flutter/pro/opensource/yx_scope/packages/yx_scope_flutter/pubspec.yaml", + "yx_scope/packages/yx_scope_flutter/pubspec_overrides.yaml":"flutter/pro/opensource/yx_scope/packages/yx_scope_flutter/pubspec_overrides.yaml", + "yx_scope/packages/yx_scope_flutter/test/scope_builder_test.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope_flutter/test/scope_builder_test.dart", + "yx_scope/packages/yx_scope_flutter/test/scope_listener_test.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope_flutter/test/scope_listener_test.dart", + "yx_scope/packages/yx_scope_flutter/test/scope_provider_test.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope_flutter/test/scope_provider_test.dart", + "yx_scope/packages/yx_scope_flutter/test/test_utils.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope_flutter/test/test_utils.dart", + "yx_scope/packages/yx_scope_linter/.gitignore":"flutter/pro/opensource/yx_scope/packages/yx_scope_linter/.gitignore", + "yx_scope/packages/yx_scope_linter/.metadata":"flutter/pro/opensource/yx_scope/packages/yx_scope_linter/.metadata", + "yx_scope/packages/yx_scope_linter/CHANGELOG.md":"flutter/pro/opensource/yx_scope/packages/yx_scope_linter/CHANGELOG.md", + "yx_scope/packages/yx_scope_linter/LICENSE":"flutter/pro/opensource/yx_scope/packages/yx_scope_linter/LICENSE", + "yx_scope/packages/yx_scope_linter/README.md":"flutter/pro/opensource/yx_scope/packages/yx_scope_linter/README.md", + "yx_scope/packages/yx_scope_linter/analysis_options.yaml":"flutter/pro/opensource/yx_scope/packages/yx_scope_linter/analysis_options.yaml", + "yx_scope/packages/yx_scope_linter/doc/manual_linter.md":"flutter/pro/opensource/yx_scope/packages/yx_scope_linter/doc/manual_linter.md", + "yx_scope/packages/yx_scope_linter/example/.gitignore":"flutter/pro/opensource/yx_scope/packages/yx_scope_linter/example/.gitignore", + "yx_scope/packages/yx_scope_linter/example/.metadata":"flutter/pro/opensource/yx_scope/packages/yx_scope_linter/example/.metadata", + "yx_scope/packages/yx_scope_linter/example/CHANGELOG.md":"flutter/pro/opensource/yx_scope/packages/yx_scope_linter/example/CHANGELOG.md", + "yx_scope/packages/yx_scope_linter/example/LICENSE":"flutter/pro/opensource/yx_scope/packages/yx_scope_linter/example/LICENSE", + "yx_scope/packages/yx_scope_linter/example/README.md":"flutter/pro/opensource/yx_scope/packages/yx_scope_linter/example/README.md", + "yx_scope/packages/yx_scope_linter/example/analysis_options.yaml":"flutter/pro/opensource/yx_scope/packages/yx_scope_linter/example/analysis_options.yaml", + "yx_scope/packages/yx_scope_linter/example/lib/avoid_async_dep_child_scope.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope_linter/example/lib/avoid_async_dep_child_scope.dart", + "yx_scope/packages/yx_scope_linter/example/lib/consider_dep_suffix.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope_linter/example/lib/consider_dep_suffix.dart", + "yx_scope/packages/yx_scope_linter/example/lib/dep_cycle.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope_linter/example/lib/dep_cycle.dart", + "yx_scope/packages/yx_scope_linter/example/lib/final_dep.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope_linter/example/lib/final_dep.dart", + "yx_scope/packages/yx_scope_linter/example/lib/pass_async_lifecycle_in_initialize_queue.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope_linter/example/lib/pass_async_lifecycle_in_initialize_queue.dart", + "yx_scope/packages/yx_scope_linter/example/lib/use_async_dep_for_async_lifecycle.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope_linter/example/lib/use_async_dep_for_async_lifecycle.dart", + "yx_scope/packages/yx_scope_linter/example/lib/utils.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope_linter/example/lib/utils.dart", + "yx_scope/packages/yx_scope_linter/example/pubspec.yaml":"flutter/pro/opensource/yx_scope/packages/yx_scope_linter/example/pubspec.yaml", + "yx_scope/packages/yx_scope_linter/example/pubspec_overrides.yaml":"flutter/pro/opensource/yx_scope/packages/yx_scope_linter/example/pubspec_overrides.yaml", + "yx_scope/packages/yx_scope_linter/lib/src/extensions.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope_linter/lib/src/extensions.dart", + "yx_scope/packages/yx_scope_linter/lib/src/lints/avoid_async_dep_child_scope.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope_linter/lib/src/lints/avoid_async_dep_child_scope.dart", + "yx_scope/packages/yx_scope_linter/lib/src/lints/consider_dep_suffix.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope_linter/lib/src/lints/consider_dep_suffix.dart", + "yx_scope/packages/yx_scope_linter/lib/src/lints/dep_cycle.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope_linter/lib/src/lints/dep_cycle.dart", + "yx_scope/packages/yx_scope_linter/lib/src/lints/final_dep.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope_linter/lib/src/lints/final_dep.dart", + "yx_scope/packages/yx_scope_linter/lib/src/lints/pass_async_lifecycle_in_initialize_queue.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope_linter/lib/src/lints/pass_async_lifecycle_in_initialize_queue.dart", + "yx_scope/packages/yx_scope_linter/lib/src/lints/use_async_dep_for_async_lifecycle.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope_linter/lib/src/lints/use_async_dep_for_async_lifecycle.dart", + "yx_scope/packages/yx_scope_linter/lib/src/models/dep.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope_linter/lib/src/models/dep.dart", + "yx_scope/packages/yx_scope_linter/lib/src/models/exceptions.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope_linter/lib/src/models/exceptions.dart", + "yx_scope/packages/yx_scope_linter/lib/src/names.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope_linter/lib/src/names.dart", + "yx_scope/packages/yx_scope_linter/lib/src/plugin.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope_linter/lib/src/plugin.dart", + "yx_scope/packages/yx_scope_linter/lib/src/priority.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope_linter/lib/src/priority.dart", + "yx_scope/packages/yx_scope_linter/lib/src/resolved_yx_scope_result.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope_linter/lib/src/resolved_yx_scope_result.dart", + "yx_scope/packages/yx_scope_linter/lib/src/types.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope_linter/lib/src/types.dart", + "yx_scope/packages/yx_scope_linter/lib/src/utils.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope_linter/lib/src/utils.dart", + "yx_scope/packages/yx_scope_linter/lib/src/visitors/parse_dependencies_for_dep_visitor.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope_linter/lib/src/visitors/parse_dependencies_for_dep_visitor.dart", + "yx_scope/packages/yx_scope_linter/lib/src/visitors/parse_initialize_queue_visitor.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope_linter/lib/src/visitors/parse_initialize_queue_visitor.dart", + "yx_scope/packages/yx_scope_linter/lib/src/visitors/parse_scope_declaration_visitor.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope_linter/lib/src/visitors/parse_scope_declaration_visitor.dart", + "yx_scope/packages/yx_scope_linter/lib/src/visitors/parse_visitor.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope_linter/lib/src/visitors/parse_visitor.dart", + "yx_scope/packages/yx_scope_linter/lib/src/yx_scope_lint_rule.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope_linter/lib/src/yx_scope_lint_rule.dart", + "yx_scope/packages/yx_scope_linter/lib/src/yx_scope_registry.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope_linter/lib/src/yx_scope_registry.dart", + "yx_scope/packages/yx_scope_linter/lib/yx_scope_linter.dart":"flutter/pro/opensource/yx_scope/packages/yx_scope_linter/lib/yx_scope_linter.dart", + "yx_scope/packages/yx_scope_linter/pubspec.yaml":"flutter/pro/opensource/yx_scope/packages/yx_scope_linter/pubspec.yaml", + "yx_scope/packages/yx_scope_linter/pubspec_overrides.yaml":"flutter/pro/opensource/yx_scope/packages/yx_scope_linter/pubspec_overrides.yaml", + "yx_state/.gitignore":"flutter/pro/opensource/yx_state/.gitignore", + "yx_state/AUTHORS":"flutter/pro/opensource/yx_state/AUTHORS", + "yx_state/CONTRIBUTING.md":"flutter/pro/opensource/yx_state/CONTRIBUTING.md", + "yx_state/LICENSE":"flutter/pro/opensource/yx_state/LICENSE", + "yx_state/README.md":"flutter/pro/opensource/yx_state/README.md", + "yx_state/analysis_options.yaml":"flutter/pro/opensource/yx_state/analysis_options.yaml", + "yx_state/assets/logos/yx_state.webp":"flutter/pro/opensource/yx_state/assets/logos/yx_state.webp", + "yx_state/packages/yx_state/.gitignore":"flutter/pro/opensource/yx_state/packages/yx_state/.gitignore", + "yx_state/packages/yx_state/AUTHORS":"flutter/pro/opensource/yx_state/packages/yx_state/AUTHORS", + "yx_state/packages/yx_state/CHANGELOG.md":"flutter/pro/opensource/yx_state/packages/yx_state/CHANGELOG.md", + "yx_state/packages/yx_state/CONTRIBUTING.md":"flutter/pro/opensource/yx_state/packages/yx_state/CONTRIBUTING.md", + "yx_state/packages/yx_state/LICENSE":"flutter/pro/opensource/yx_state/packages/yx_state/LICENSE", + "yx_state/packages/yx_state/README.md":"flutter/pro/opensource/yx_state/packages/yx_state/README.md", + "yx_state/packages/yx_state/analysis_options.yaml":"flutter/pro/opensource/yx_state/packages/yx_state/analysis_options.yaml", + "yx_state/packages/yx_state/example/main.dart":"flutter/pro/opensource/yx_state/packages/yx_state/example/main.dart", + "yx_state/packages/yx_state/lib/src/base/interface.dart":"flutter/pro/opensource/yx_state/packages/yx_state/lib/src/base/interface.dart", + "yx_state/packages/yx_state/lib/src/base/state_manager_base.dart":"flutter/pro/opensource/yx_state/packages/yx_state/lib/src/base/state_manager_base.dart", + "yx_state/packages/yx_state/lib/src/function_stream_handler/handle_task.dart":"flutter/pro/opensource/yx_state/packages/yx_state/lib/src/function_stream_handler/handle_task.dart", + "yx_state/packages/yx_state/lib/src/function_stream_handler/handle_task_emitter.dart":"flutter/pro/opensource/yx_state/packages/yx_state/lib/src/function_stream_handler/handle_task_emitter.dart", + "yx_state/packages/yx_state/lib/src/function_stream_handler/stream_function_handler.dart":"flutter/pro/opensource/yx_state/packages/yx_state/lib/src/function_stream_handler/stream_function_handler.dart", + "yx_state/packages/yx_state/lib/src/mixin/state_manager_listener_mixin.dart":"flutter/pro/opensource/yx_state/packages/yx_state/lib/src/mixin/state_manager_listener_mixin.dart", + "yx_state/packages/yx_state/lib/src/state_manager.dart":"flutter/pro/opensource/yx_state/packages/yx_state/lib/src/state_manager.dart", + "yx_state/packages/yx_state/lib/src/state_manager_observer.dart":"flutter/pro/opensource/yx_state/packages/yx_state/lib/src/state_manager_observer.dart", + "yx_state/packages/yx_state/lib/src/state_manager_overrides.dart":"flutter/pro/opensource/yx_state/packages/yx_state/lib/src/state_manager_overrides.dart", + "yx_state/packages/yx_state/lib/src/test_util/state_manager_base_test_util.dart":"flutter/pro/opensource/yx_state/packages/yx_state/lib/src/test_util/state_manager_base_test_util.dart", + "yx_state/packages/yx_state/lib/yx_state.dart":"flutter/pro/opensource/yx_state/packages/yx_state/lib/yx_state.dart", + "yx_state/packages/yx_state/pubspec.yaml":"flutter/pro/opensource/yx_state/packages/yx_state/pubspec.yaml", + "yx_state/packages/yx_state/screenshots/yx_state_logo.webp":"flutter/pro/opensource/yx_state/packages/yx_state/screenshots/yx_state_logo.webp", + "yx_state/packages/yx_state/test/src/function_stream_handler/handle_task_emitter_test.dart":"flutter/pro/opensource/yx_state/packages/yx_state/test/src/function_stream_handler/handle_task_emitter_test.dart", + "yx_state/packages/yx_state/test/src/function_stream_handler/handle_task_test.dart":"flutter/pro/opensource/yx_state/packages/yx_state/test/src/function_stream_handler/handle_task_test.dart", + "yx_state/packages/yx_state/test/src/function_stream_handler/stream_function_handler_test.dart":"flutter/pro/opensource/yx_state/packages/yx_state/test/src/function_stream_handler/stream_function_handler_test.dart", + "yx_state/packages/yx_state/test/src/mixin/state_manager_listener_mixin_test.dart":"flutter/pro/opensource/yx_state/packages/yx_state/test/src/mixin/state_manager_listener_mixin_test.dart", + "yx_state/packages/yx_state/test/src/mocks.dart":"flutter/pro/opensource/yx_state/packages/yx_state/test/src/mocks.dart", + "yx_state/packages/yx_state/test/src/state_manager_test.dart":"flutter/pro/opensource/yx_state/packages/yx_state/test/src/state_manager_test.dart", + "yx_state/packages/yx_state_flutter/.gitignore":"flutter/pro/opensource/yx_state/packages/yx_state_flutter/.gitignore", + "yx_state/packages/yx_state_flutter/AUTHORS":"flutter/pro/opensource/yx_state/packages/yx_state_flutter/AUTHORS", + "yx_state/packages/yx_state_flutter/CHANGELOG.md":"flutter/pro/opensource/yx_state/packages/yx_state_flutter/CHANGELOG.md", + "yx_state/packages/yx_state_flutter/CONTRIBUTING.md":"flutter/pro/opensource/yx_state/packages/yx_state_flutter/CONTRIBUTING.md", + "yx_state/packages/yx_state_flutter/LICENSE":"flutter/pro/opensource/yx_state/packages/yx_state_flutter/LICENSE", + "yx_state/packages/yx_state_flutter/README.md":"flutter/pro/opensource/yx_state/packages/yx_state_flutter/README.md", + "yx_state/packages/yx_state_flutter/analysis_options.yaml":"flutter/pro/opensource/yx_state/packages/yx_state_flutter/analysis_options.yaml", + "yx_state/packages/yx_state_flutter/example/.gitignore":"flutter/pro/opensource/yx_state/packages/yx_state_flutter/example/.gitignore", + "yx_state/packages/yx_state_flutter/example/README.md":"flutter/pro/opensource/yx_state/packages/yx_state_flutter/example/README.md", + "yx_state/packages/yx_state_flutter/example/analysis_options.yaml":"flutter/pro/opensource/yx_state/packages/yx_state_flutter/example/analysis_options.yaml", + "yx_state/packages/yx_state_flutter/example/lib/main.dart":"flutter/pro/opensource/yx_state/packages/yx_state_flutter/example/lib/main.dart", + "yx_state/packages/yx_state_flutter/example/pubspec.yaml":"flutter/pro/opensource/yx_state/packages/yx_state_flutter/example/pubspec.yaml", + "yx_state/packages/yx_state_flutter/example/pubspec_overrides.yaml":"flutter/pro/opensource/yx_state/packages/yx_state_flutter/example/pubspec_overrides.yaml", + "yx_state/packages/yx_state_flutter/lib/src/state_builder.dart":"flutter/pro/opensource/yx_state/packages/yx_state_flutter/lib/src/state_builder.dart", + "yx_state/packages/yx_state_flutter/lib/src/state_consumer.dart":"flutter/pro/opensource/yx_state/packages/yx_state_flutter/lib/src/state_consumer.dart", + "yx_state/packages/yx_state_flutter/lib/src/state_listener.dart":"flutter/pro/opensource/yx_state/packages/yx_state_flutter/lib/src/state_listener.dart", + "yx_state/packages/yx_state_flutter/lib/src/state_selector.dart":"flutter/pro/opensource/yx_state/packages/yx_state_flutter/lib/src/state_selector.dart", + "yx_state/packages/yx_state_flutter/lib/src/typedefs.dart":"flutter/pro/opensource/yx_state/packages/yx_state_flutter/lib/src/typedefs.dart", + "yx_state/packages/yx_state_flutter/lib/yx_state_flutter.dart":"flutter/pro/opensource/yx_state/packages/yx_state_flutter/lib/yx_state_flutter.dart", + "yx_state/packages/yx_state_flutter/pubspec.yaml":"flutter/pro/opensource/yx_state/packages/yx_state_flutter/pubspec.yaml", + "yx_state/packages/yx_state_flutter/pubspec_overrides.yaml":"flutter/pro/opensource/yx_state/packages/yx_state_flutter/pubspec_overrides.yaml", + "yx_state/packages/yx_state_transformers/.gitignore":"flutter/pro/opensource/yx_state/packages/yx_state_transformers/.gitignore", + "yx_state/packages/yx_state_transformers/AUTHORS":"flutter/pro/opensource/yx_state/packages/yx_state_transformers/AUTHORS", + "yx_state/packages/yx_state_transformers/CHANGELOG.md":"flutter/pro/opensource/yx_state/packages/yx_state_transformers/CHANGELOG.md", + "yx_state/packages/yx_state_transformers/CONTRIBUTING.md":"flutter/pro/opensource/yx_state/packages/yx_state_transformers/CONTRIBUTING.md", + "yx_state/packages/yx_state_transformers/LICENSE":"flutter/pro/opensource/yx_state/packages/yx_state_transformers/LICENSE", + "yx_state/packages/yx_state_transformers/README.md":"flutter/pro/opensource/yx_state/packages/yx_state_transformers/README.md", + "yx_state/packages/yx_state_transformers/analysis_options.yaml":"flutter/pro/opensource/yx_state/packages/yx_state_transformers/analysis_options.yaml", + "yx_state/packages/yx_state_transformers/example/main.dart":"flutter/pro/opensource/yx_state/packages/yx_state_transformers/example/main.dart", + "yx_state/packages/yx_state_transformers/lib/src/function_handlers.dart":"flutter/pro/opensource/yx_state/packages/yx_state_transformers/lib/src/function_handlers.dart", + "yx_state/packages/yx_state_transformers/lib/src/handle_task_transformers.dart":"flutter/pro/opensource/yx_state/packages/yx_state_transformers/lib/src/handle_task_transformers.dart", + "yx_state/packages/yx_state_transformers/lib/yx_state_transformers.dart":"flutter/pro/opensource/yx_state/packages/yx_state_transformers/lib/yx_state_transformers.dart", + "yx_state/packages/yx_state_transformers/pubspec.yaml":"flutter/pro/opensource/yx_state/packages/yx_state_transformers/pubspec.yaml", + "yx_state/packages/yx_state_transformers/pubspec_overrides.yaml":"flutter/pro/opensource/yx_state/packages/yx_state_transformers/pubspec_overrides.yaml" +} \ No newline at end of file diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/.piglet-meta.json b/mnemo_cards_web_v2/packages/yx/city-services-pub/.piglet-meta.json new file mode 100644 index 0000000..404924d --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/.piglet-meta.json @@ -0,0 +1,4 @@ +{ + "project":"city-services-pub", + "repository":"arcadia" +} \ No newline at end of file diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/CONTRIBUTING.md b/mnemo_cards_web_v2/packages/yx/city-services-pub/CONTRIBUTING.md new file mode 100644 index 0000000..29badd6 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/CONTRIBUTING.md @@ -0,0 +1,31 @@ +## Notice to external contributors +### General info +Hello! In order for us (YANDEX LLC) to accept patches and other contributions from you, you will have to adopt our Contributor License Agreement (the “CLA”). The current version of the CLA you may find here: + +* https://yandex.ru/legal/cla/?lang=en (in English) +* https://yandex.ru/legal/cla/?lang=ru (in Russian). + +By adopting the CLA, you state the following: + +* You obviously wish and are willingly licensing your contributions to us for our open source projects under the terms of the CLA, +* You have read the terms and conditions of the CLA and agree with them in full, +* You are legally able to provide and license your contributions as stated, +* We may use your contributions for our open source projects and for any other our project too, +* We rely on your assurances concerning the rights of third parties in relation to your contributions. + +If you agree with these principles, please read and adopt our CLA. By providing us your contributions, you hereby declare that you have read and adopted our CLA, and we may freely merge your contributions with our corresponding open source project and use it in further in accordance with terms and conditions of the CLA. + +### Provide contributions +If you have adopted terms and conditions of the CLA, you are able to provide your contributions. When you submit your pull request, please add the following information into it: + +``` +I hereby agree to the terms of the CLA available at: [link]. +``` + +Replace the bracketed text as follows: + +* [link] is the link at the current version of the CLA (you may add here a link https://yandex.ru/legal/cla/?lang=en (in English) or a link https://yandex.ru/legal/cla/?lang=ru (in Russian). + It is enough to provide us with such notification once. + +### Other questions +If you have any questions, please write us at opensource@yandex-team.ru. \ No newline at end of file diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/README.md b/mnemo_cards_web_v2/packages/yx/city-services-pub/README.md new file mode 100644 index 0000000..9d8d3c9 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/README.md @@ -0,0 +1,19 @@ +# City Services Flutter™ Libraries + +This repository contains a collection of Flutter™ libraries developed by Yandex City Services. These libraries provide reusable components and utilities for building Flutter mobile applications. + +## Usage + +Each library contains its own documentation and usage examples. Please refer to individual directories for specific implementation details. + +## **Contributing** + +We welcome contributions from the community. Please read our contributing guidelines before submitting pull requests. + +## **License** + +Please check individual package licenses for specific terms and conditions. + +--- + +**Disclaimer:** Flutter and the related logo are trademarks of Google LLC. We are not endorsed by or affiliated with Google LLC. \ No newline at end of file diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/.gitignore b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/.gitignore new file mode 100644 index 0000000..b1f539c --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/.gitignore @@ -0,0 +1,30 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +.packages +build/ +.arcignore diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/AUTHORS b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/AUTHORS new file mode 100644 index 0000000..4814aba --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/AUTHORS @@ -0,0 +1,9 @@ +The following authors have created the source code of "yx_scope" published and distributed by YANDEX LLC as the owner: +kltsv +FedorZavalnyj +wupididu + +The following authors have licensed their contributions to YANDEX LLC and everyone who uses "yx_scope" under the licensing terms detailed in LICENSE available at https://github.com/yandex/yx_scope/blob/main/LICENSE. +kltsv +FedorZavalnyj +wupididu diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/CONTRIBUTING.md b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/CONTRIBUTING.md new file mode 100644 index 0000000..3cef4d8 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/CONTRIBUTING.md @@ -0,0 +1,31 @@ +## Notice to external contributors +### General info +Hello! In order for us (YANDEX LLC) to accept patches and other contributions from you, you will have to adopt our Contributor License Agreement (the “CLA”). The current version of the CLA you may find here: + +* https://yandex.ru/legal/cla/?lang=en (in English) +* https://yandex.ru/legal/cla/?lang=ru (in Russian). + +By adopting the CLA, you state the following: + +* You obviously wish and are willingly licensing your contributions to us for our open source projects under the terms of the CLA, +* You have read the terms and conditions of the CLA and agree with them in full, +* You are legally able to provide and license your contributions as stated, +* We may use your contributions for our open source projects and for any other our project too, +* We rely on your assurances concerning the rights of third parties in relation to your contributions. + +If you agree with these principles, please read and adopt our CLA. By providing us your contributions, you hereby declare that you have read and adopted our CLA, and we may freely merge your contributions with our corresponding open source project and use it in further in accordance with terms and conditions of the CLA. + +### Provide contributions +If you have adopted terms and conditions of the CLA, you are able to provide your contributions. When you submit your pull request, please add the following information into it: + +``` +I hereby agree to the terms of the CLA available at: [link]. +``` + +Replace the bracketed text as follows: + +* [link] is the link at the current version of the CLA (you may add here a link https://yandex.ru/legal/cla/?lang=en (in English) or a link https://yandex.ru/legal/cla/?lang=ru (in Russian). + It is enough to provide us with such notification once. + +### Other questions +If you have any questions, please write us at opensource@yandex-team.ru. diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/LICENSE b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/LICENSE new file mode 100644 index 0000000..bd7b143 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2024 YANDEX LLC + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/README.md b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/README.md new file mode 100644 index 0000000..b97323b --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/README.md @@ -0,0 +1,28 @@ +# yx_scope packages + +yx_scope is a compile-safe DI framework with advanced scoping capabilities. + +## Library Components + +The library group currently consists of: + +- **[yx_scope](packages/yx_scope)**: The core implementation of the framework +- **[yx_scope_flutter](packages/yx_scope_flutter)**: An adapter library that allows embedding + yx_scope containers into the widget tree +- **[yx_scope_linter](packages/yx_scope_linter)**: A set of custom lint rules that provide + additional protection against errors when working with yx_scope + +## Features + +- Pure Dart +- DI-like (not static and not ServiceLocator) +- Compile-safe access to dependencies +- No code generation +- Flutter-friendly container management +- Declarative description of the dependency tree +- Non-reactive dependency tree +- Unambiguous behavior and lifecycle of dependencies in containers +- Ability to create scopes of any nesting level +- Compile-safe check for the existence of active scopes +- Support for asynchronous dependencies and their initialization +- Compile-safe protection against circular dependencies diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/assets/logos/yx_scope.webp b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/assets/logos/yx_scope.webp new file mode 100644 index 0000000..74536ea Binary files /dev/null and b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/assets/logos/yx_scope.webp differ diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/.gitignore b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/.gitignore new file mode 100644 index 0000000..4915961 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/.gitignore @@ -0,0 +1,39 @@ +# Ignoring native folders of the example as they can be re-generated easily using: +# flutter create --platforms=android,ios,web,windows,macos . +example/android/ +example/ios/ +example/web/ +example/windows/ +example/macos/ +example/linux/ + +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +.dart_tool/ +.packages +build/ +/pubspec.lock + +.arcignore diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/.metadata b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/.metadata new file mode 100644 index 0000000..dfb6a8f --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: cfa03f886884e42676f1834864657721a1219624 + channel: stable + +project_type: package diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/CHANGELOG.md b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/CHANGELOG.md new file mode 100644 index 0000000..98bfaa9 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/CHANGELOG.md @@ -0,0 +1,89 @@ +## 1.1.3 - unpublished +* Added CustomDep/CustomAsyncDep classes for creating custom dependency types + +## 1.1.2 - 2025.08.01 + +* Fix: ScopeListners/DepListeners/AsyncDepListeners usage + +## 1.1.1 - 2025.07.08 + +* Documentation links fixed + +## 1.1.0 - 2025.07.07 + +* Public sealed scope state API added +* Fix: Prohibit DataScope initialization with null data by enforcing non-nullable Data type +* Documentation fixes and updates +* Updated CI with new Flutter version support + +#### Deprecated API +Listeners have been renamed into Observers due to conflicts with yx_scope_flutter package. +Consider using Observers instead of Listeners. +Deprecated classes and fields will be highlighted from now on. +In the next major, deprecated API will be completely removed. + +## 1.0.2 - 2024.12.18 + +* Fix: corrected an inheritance of DataScopeContainer class to BaseScopeContainer instead of + ScopeContainer +* Updated repository link +* Library topics added + +## 1.0.1 - 2024.10.23 + +* Minor fixes in formatting and links + +## 1.0.0 - 2024.10.18 + +* Remove redundant register/unregister listener methods +* Ready to be open-source + +## 0.1.4 - 2024.10.10 + +* yx_scoped -> yx_scope + +## 0.1.3 - 2024.09.23 + +* AsyncDep init/dispose callback now are hidden from API +* @nonVirtual nonVirtual for dep/asyncDep/rawAsyncDep methods + +## 0.1.2 - 2024.08.20 + +* Added optional `name` + for `ScopeContainer`, `ChildScopeContainer`, `DataScopeContainer`, `ChildDataScopeContainer` + +## 0.1.1 - 2024.05.27 + +* Generic type of container for ScopeHolder's changed for strict link ScopedHolder's with + ScopeContainer's + +## 0.1.0 — 2024.05.08 + +1. All -Node entities has been renamed to -Container +2. Reorder parent and data args in createContainer methods + +## 0.0.4 — 2024.03.09 + +Core entities has been renamed: + +1. ScopeModule -> BaseScopeNode +2. FeatureScopeModule -> ScopeModule +3. RootScopeModule -> ScopeNode +4. DataChildScopeModule -> ChildDataScopeModule + +And all their children has been renamed accordingly. + +## 0.0.3 — 2024.03.04 + +* New class available: FeatureScopeModule. It helps to decompose ScopeModule into a number of + features. +* Unnecessary build.yaml removed + +## 0.0.2 — 2024.02.28 + +* Parent scope can be defined as an abstract interface that implements BaseScope. It helps to unbind + child from knowing an exact implementation of the parent and depend only on it's interface. + +## 0.0.1 — 2024.02.04 + +* Initial release diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/LICENSE b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/LICENSE new file mode 100644 index 0000000..bd7b143 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2024 YANDEX LLC + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/README.md b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/README.md new file mode 100644 index 0000000..70cb59b --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/README.md @@ -0,0 +1,121 @@ +## yx_scope + +
+The yx_scope package logo + +**yx_scope is a compile-safe DI framework with advanced scoping capabilities.** + +[![Pub Version](https://img.shields.io/pub/v/yx_scope)](https://pub.dev/packages/yx_scope) +
+ +## Library Components + +The library group currently consists of: + +- **yx_scope**: The core implementation of the framework +- **yx_scope_flutter**: An adapter library that allows embedding yx_scope containers into the widget + tree +- **yx_scope_linter**: A set of custom lint rules that provide additional protection against errors + when working with yx_scope + +## Features + +- Pure Dart +- DI-like (not static and not ServiceLocator) +- Compile-safe access to dependencies +- No code generation +- Flutter-friendly container management +- Declarative description of the dependency tree +- Non-reactive dependency tree +- Unambiguous behavior and lifecycle of dependencies in containers +- Ability to create scopes of any nesting level +- Compile-safe check for the existence of active scopes +- Support for asynchronous dependencies and their initialization +- Compile-safe protection against circular dependencies + +## Quick Start + +Let's look at a simple dependency container. First, add yx_scope to your pubspec.yaml: + +```yaml +dependencies: + yx_scope: ^1.0.0 +``` + +Create a file named `app_scope.dart` and add the description of our container and dependencies: + +```dart +class AppScopeContainer extends ScopeContainer { + late final routerDelegateDep = dep(() => AppRouterDelegate()); + + late final appStateObserverDep = dep( + () => + AppStateObserver( + routerDelegateDep.get, + ), + ); +} + +class AppScopeHolder extends ScopeHolder { + @override + AppScopeContainer createContainer() => AppScopeContainer(); +} +``` + +Now, let's create an `AppScopeHolder`, create a container, and access the dependencies: + +```dart +void main() async { + final appScopeHolder = AppScopeHolder(); + await appScopeHolder.create(); + + final appScope = appScopeHolder.scope; + if (appScope != null) { + final AppStateObserver appStateObserver = appScope.appStateObserverDep.get; + } +} +``` + +An important feature of the library is that we work with the DI container without binding it to the +UI. The DI container is primary, and only as an addition to this, the container can be attached to +the UI. + +The DI container is created as a reaction to an event in the application's logic, not as a result of +the appearance of some screen or UI element. UI does not generate scopes; scopes generate UI. + +This is an important principle of the mechanics of yx_scope. + +## Key Entities + +- **Dep (dependency)**: A container for one specific instance of any entity. +- **ScopeContainer**: An isolated, non-overlapping set of dependencies united by a meaningful scope + and sharing a common lifecycle. +- **ScopeHolder**: An instance that stores the current state of the container and is responsible for + its initialization and disposal. + +> A ScopeContainer can be closed by some public interface, hiding implementation details and access +> to Dep. In this case, the interface for it will have the suffix Scope, for example, AccountScope. + +`ScopeHolder` is responsible for creating and removing the scope using the `create` and `drop` +methods. + +Initially, `ScopeHolder` contains a null state. + +After calling the `create` method, a scope appears - a container with dependencies that can be +accessed through the `ScopeHolder`. + +After the `drop` method, all scope dependencies are disposed - the `ScopeHolder` again contains +null. + +Due to null-safety, `ScopeHolder` provides a compile-safe check for the existence of a scope +directly +at the time of writing code, not at runtime. + +![Scope Anatomy](https://github.com/yandex/city-services-pub/blob/main/yx_scope/packages/yx_scope/doc/assets/scope_anatomy.png?raw=true) + +## Live Templates + +You can write code of scopes faster with Live Templates for Intellij IDEA/Android Studio or VSCode. + +You can find an +instruction [in the documentation](https://github.com/yandex/city-services-pub/blob/main/yx_scope/packages/yx_scope/doc/live_templates.md). \ No newline at end of file diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/analysis_options.yaml b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/analysis_options.yaml new file mode 100644 index 0000000..572dd23 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/analysis_options.yaml @@ -0,0 +1 @@ +include: package:lints/recommended.yaml diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/doc/assets/scope_anatomy.png b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/doc/assets/scope_anatomy.png new file mode 100644 index 0000000..07b5d14 Binary files /dev/null and b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/doc/assets/scope_anatomy.png differ diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/doc/live_templates.md b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/doc/live_templates.md new file mode 100644 index 0000000..d158945 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/doc/live_templates.md @@ -0,0 +1,264 @@ +# Intellij IDEA / Android Studio + +1. Go to IntelliJ IDEA > Settings (or IntelliJ IDEA > Preferences on macOS) to open the Settings. +2. In the Settings window, navigate to Editor > Live Templates. +3. Click Add (plus button), select Template Group and give it a name "yx_scope". +4. Select created group and click Add button one more time — select Live Template. +5. For each template below repeat the following steps +6. Add an Abbreviation (the title of the snippet below) and copy the snippet to the Template Text. +7. There is a text "No applicable contexts" — click "Define" and select Dart. + +Now you can type abbreviations in any file of your project and apply the snippet by clicking Enter. + +## scope + +```dart +import 'package:meta/meta.dart'; +import 'package:yx_scope/yx_scope.dart'; + +class $NAME$ScopeContainer extends ScopeContainer { + // TODO deps +} + +class $NAME$ScopeHolder extends ScopeHolder<$NAME$ScopeContainer> { + @protected + @override + $NAME$ScopeContainer createContainer() => $NAME$ScopeContainer(); +} +``` + +## scope_child + +```dart +import 'package:meta/meta.dart'; +import 'package:yx_scope/yx_scope.dart'; + +class $NAME$ScopeContainer extends ChildScopeContainer<$PARENTNAME$> { + $NAME$ScopeContainer({required $PARENTNAME$ parent}) : super(parent: parent); + +// TODO deps +} + +class $NAME$ScopeHolder extends ChildScopeHolder<$NAME$ScopeContainer, $PARENTNAME$> { + $NAME$ScopeHolder($PARENTNAME$ parent) : super(parent); + + @protected + @override + $NAME$ScopeContainer createContainer($PARENTNAME$ parent) => + $NAME$ScopeContainer(parent: parent); +} +``` + +## scope_data + +```dart +import 'package:meta/meta.dart'; +import 'package:yx_scope/yx_scope.dart'; + +class $NAME$ScopeContainer extends DataScopeContainer<$DATA$> { + $NAME$ScopeContainer({required $DATA$ data}) : super(data: data); + +// TODO deps +} + +class $NAME$ScopeHolder extends DataScopeHolder<$NAME$ScopeContainer, $DATA$> { + @protected + @override + $NAME$ScopeContainer createContainer($DATA$ data) => $NAME$ScopeContainer(data: data); +} +``` + +## scope_child_data + +```dart +import 'package:meta/meta.dart'; +import 'package:yx_scope/yx_scope.dart'; + +class $NAME$ScopeContainer extends ChildDataScopeContainer<$PARENTNAME$, $DATA$> { + $NAME$ScopeContainer({ + required $PARENTNAME$ parent, + required $DATA$ data, + }) : super(parent: parent, data: data); + +// TODO deps +} + +class $NAME$ScopeHolder extends ChildDataScopeHolder<$NAME$ScopeContainer, + $PARENTNAME$, + $DATA$> { + $NAME$ScopeHolder($PARENTNAME$ parent) : super(parent); + + @protected + @override + $NAME$ScopeContainer createContainer($PARENTNAME$ parent, + $DATA$ data,) => + $NAME$ScopeContainer(parent: parent, data: data); +} +``` + +## dep + +```dart + +late final $NAME$Dep = dep(() => $INSTANCE$); +``` + +## dep_async + +```dart + +late final $NAME$Dep = asyncDep(() => $INSTANCE$); +``` + +## dep_raw_async + +```dart + +late final $NAME$Dep = rawAsyncDep( + () => $INSTANCE$, + init: (value) async { + // init + }, + dispose: (value) async { + // dispose + }, +); +``` + +# VSCode + +1. Go to File > Preferences > Configure Snippets. +2. Select "New global snippets file" or "New snippets file for [your project]" +3. Enter the name of the file +4. Copy and paste the entire code below +5. Save file + +Now you can type keys from that json in any file of your project and apply the snippet by clicking +Enter. + +``` +{ + "scope": { + "prefix": "scope", + "body": [ + "import 'package:meta/meta.dart';", + "import 'package:yx_scope/yx_scope.dart';", + "", + "class ${1:Name}ScopeContainer extends ScopeContainer {", + " // TODO deps", + "}", + "", + "class ${1:Name}ScopeHolder extends ScopeHolder<${1:Name}ScopeContainer> {", + " @protected", + " @override", + " ${1:Name}ScopeContainer createContainer() => ${1:Name}ScopeContainer();", + "}" + ], + "description": "Template for scope" + }, + "scope_child": { + "prefix": "scope_child", + "body": [ + "import 'package:meta/meta.dart';", + "import 'package:yx_scope/yx_scope.dart';", + "", + "class ${1:Name}ScopeContainer extends ChildScopeContainer<${2:ParentName}> {", + " ${1:Name}ScopeContainer({required ${2:ParentName} parent}) : super(parent: parent);", + "", + " // TODO deps", + "}", + "", + "class ${1:Name}ScopeHolder", + " extends ChildScopeHolder<${1:Name}ScopeContainer, ${2:ParentName}> {", + " ${1:Name}ScopeHolder(${2:ParentName} parent) : super(parent);", + " @protected", + " @override", + " ${1:Name}ScopeContainer createContainer(${2:ParentName} parent) =>", + " ${1:Name}ScopeContainer(parent: parent);", + "}" + ], + "description": "Template for child scope" + }, + "scope_data": { + "prefix": "scope_data", + "body": [ + "import 'package:meta/meta.dart';", + "import 'package:yx_scope/yx_scope.dart';", + "", + "class ${1:Name}ScopeContainer extends DataScopeContainer<${2:Data}> {", + " ${1:Name}ScopeContainer({required ${2:Data} data}) : super(data: data);", + "", + " // TODO deps", + "}", + "", + "class ${1:Name}ScopeHolder extends DataScopeHolder<${1:Name}ScopeContainer, ${2:Data}> {", + " @protected", + " @override", + " ${1:Name}ScopeContainer createContainer(${2:Data} data) => ${1:Name}ScopeContainer(data: data);", + "}" + ], + "description": "Template for scope data" + }, + "scope_child_data": { + "prefix": "scope_child_data", + "body": [ + "import 'package:meta/meta.dart';", + "import 'package:yx_scope/yx_scope.dart';", + "", + "class ${1:Name}ScopeContainer", + " extends ChildDataScopeContainer<${2:ParentName}, ${3:Data}> {", + " ${1:Name}ScopeContainer({", + " required ${2:ParentName} parent,", + " required ${3:Data} data,", + " }) : super(parent: parent, data: data);", + "", + " // TODO deps", + "}", + "", + "class ${1:Name}ScopeHolder extends ChildDataScopeHolder<${1:Name}ScopeContainer,", + " ${2:ParentName}, ${3:Data}> {", + " ${1:Name}ScopeHolder(${2:ParentName} parent) : super(parent);", + " @protected", + " @override", + " ${1:Name}ScopeContainer createContainer(", + " ${2:ParentName} parent,", + " ${3:Data} data,", + " ) =>", + " ${1:Name}ScopeContainer(parent: parent, data: data);", + "}" + ], + "description": "Template for child data scope" + }, + "dep": { + "prefix": "dep", + "body": [ + "late final ${1:Name}Dep = dep(() => ${2:Instance});" + ], + "description": "Template for dependency" + }, + "dep_async": { + "prefix": "dep_async", + "body": [ + "late final ${1:Name}Dep = asyncDep(() => ${2:Instance});" + ], + "description": "Template for async dependency" + }, + "raw_async_dep": { + "prefix": "raw_async_dep", + "body": [ + "late final ${1:Name}Dep = rawAsyncDep(", + " () => ${2:Instance},", + " init: (value) async {", + " // init", + " ${3:// Initialization logic here}", + " },", + " dispose: (value) async {", + " // dispose", + " ${4:// Disposal logic here}", + " },", + ");" + ], + "description": "Template for raw async dependency with init and dispose" + } +} +``` \ No newline at end of file diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/example/.gitignore b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/example/.gitignore new file mode 100644 index 0000000..ea5d354 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/example/.gitignore @@ -0,0 +1,31 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +build/ + +.arcignore diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/example/.metadata b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/example/.metadata new file mode 100644 index 0000000..fe420e9 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/example/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "4cf269e36de2573851eaef3c763994f8f9be494d" + channel: "stable" + +project_type: package diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/example/CHANGELOG.md b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/example/CHANGELOG.md new file mode 100644 index 0000000..41cc7d8 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/example/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1 + +* TODO: Describe initial release. diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/example/LICENSE b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/example/LICENSE new file mode 100644 index 0000000..ba75c69 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/example/LICENSE @@ -0,0 +1 @@ +TODO: Add your license here. diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/example/README.md b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/example/README.md new file mode 100644 index 0000000..dbf1448 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/example/README.md @@ -0,0 +1 @@ +## An example for yx_scope diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/example/analysis_options.yaml b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/example/analysis_options.yaml new file mode 100644 index 0000000..572dd23 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/example/analysis_options.yaml @@ -0,0 +1 @@ +include: package:lints/recommended.yaml diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/example/bin/app_listener.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/example/bin/app_listener.dart new file mode 100644 index 0000000..7a69222 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/example/bin/app_listener.dart @@ -0,0 +1,111 @@ +import 'package:yx_scope/yx_scope.dart'; +import './main.dart'; + +class AppScopeHolderWithListener extends ScopeHolder { + static const _listener = AppListener(); + + AppScopeHolderWithListener() + // ignore: deprecated_member_use + : super(scopeListeners: [_listener], depListeners: [_listener]); + + @override + AppScopeContainer createContainer() => AppScopeContainer(); +} + +// ignore: deprecated_member_use +class AppListener implements ScopeListener, DepListener, AsyncDepListener { + const AppListener(); + + static void _log( + String message, [ + Object? exception, + StackTrace? stackTrace, + ]) { + print(message); + if (exception != null) { + print(exception); + if (stackTrace != null) { + print(stackTrace); + } + } + } + + @override + void onScopeStartInitialize(ScopeId scope) => + _log('[$scope] -> onScopeStartInitialize'); + + @override + void onScopeInitialized(ScopeId scope) => + _log('[$scope] -> onScopeInitialized'); + + @override + void onScopeInitializeFailed( + ScopeId scope, + Object exception, + StackTrace stackTrace, + ) => + _log('[$scope] -> onScopeInitializeFailed', exception, stackTrace); + + @override + void onScopeStartDispose(ScopeId scope) => + _log('[$scope] -> onScopeStartDispose'); + + @override + void onScopeDisposed(ScopeId scope) => _log('[$scope] -> onScopeDisposed'); + + @override + void onScopeDisposeDepFailed( + ScopeId scope, + DepId dep, + Object exception, + StackTrace stackTrace, + ) => + _log('[$scope] -> onScopeDisposeDepFailed', exception, stackTrace); + + @override + void onValueStartCreate(ScopeId scope, DepId dep) => + _log('[$scope.$dep] -> onValueStartCreate'); + + @override + void onValueCreated(ScopeId scope, DepId dep, ValueMeta? valueMeta) => + _log('[$scope.$dep] -> onValueCreated'); + + @override + void onValueCreateFailed( + ScopeId scope, + DepId dep, + Object exception, + StackTrace stackTrace, + ) => + _log('[$scope.$dep] -> onValueCreated', exception, stackTrace); + + @override + void onValueCleared(ScopeId scope, DepId dep, ValueMeta? valueMeta) => + _log('[$scope.$dep]($valueMeta) -> onValueCleared'); + + @override + void onDepDisposeFailed( + ScopeId scope, DepId dep, Object exception, StackTrace stackTrace) => + _log('[$scope.$dep] -> onDepDisposeFailed', exception, stackTrace); + + @override + void onDepDisposed(ScopeId scope, DepId dep) => + _log('[$scope.$dep] -> onDepDisposed'); + + @override + void onDepInitializeFailed( + ScopeId scope, DepId dep, Object exception, StackTrace stackTrace) => + _log('[$scope.$dep] -> onDepInitializeFailed', exception, stackTrace); + + @override + void onDepInitialized(ScopeId scope, DepId dep) => + _log('[$scope.$dep] -> onDepInitialized'); + + @override + void onDepStartDispose(ScopeId scope, DepId dep) => + _log('[$scope.$dep] -> onDepStartDispose'); + + @override + void onDepStartInitialize(ScopeId scope, DepId dep) => + _log('[$scope.$dep] -> onDepStartInitialize'); +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/example/bin/deprecated_listeners_main.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/example/bin/deprecated_listeners_main.dart new file mode 100644 index 0000000..495746b --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/example/bin/deprecated_listeners_main.dart @@ -0,0 +1,15 @@ +import 'app_listener.dart'; + +/// This file will be deleted in the next major version +/// when Listeners will be completely removed +void main() async { + final appScopeHolderWithListener = AppScopeHolderWithListener(); + + await appScopeHolderWithListener.create(); + + print(appScopeHolderWithListener.scope?.routerDelegateDep.get); + + await appScopeHolderWithListener.drop(); + + print(appScopeHolderWithListener.scope?.routerDelegateDep.get); +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/example/bin/main.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/example/bin/main.dart new file mode 100644 index 0000000..05fdd92 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/example/bin/main.dart @@ -0,0 +1,138 @@ +import 'package:yx_scope/yx_scope.dart'; + +void main() async { + final appScopeHolder = AppScopeHolder(); + + await appScopeHolder.create(); + + print(appScopeHolder.scope?.routerDelegateDep.get); + + await appScopeHolder.drop(); + + print(appScopeHolder.scope?.routerDelegateDep.get); +} + +class AppRouterDelegate {} + +class AppStateObserver { + final AppRouterDelegate appRouteDelegate; + + AppStateObserver(this.appRouteDelegate); +} + +class AppScopeContainer extends ScopeContainer { + late final routerDelegateDep = dep(() => AppRouterDelegate()); + + late final appStateObserverDep = dep( + () => AppStateObserver( + routerDelegateDep.get, + ), + ); +} + +class AppScopeHolder extends ScopeHolder { + static const _observer = AppObserver(); + + AppScopeHolder() + : super(scopeObservers: [_observer], depObservers: [_observer]); + + @override + AppScopeContainer createContainer() => AppScopeContainer(); +} + +class AppObserver implements ScopeObserver, DepObserver, AsyncDepObserver { + const AppObserver(); + + static void _log( + String message, [ + Object? exception, + StackTrace? stackTrace, + ]) { + print(message); + if (exception != null) { + print(exception); + if (stackTrace != null) { + print(stackTrace); + } + } + } + + @override + void onScopeStartInitialize(ScopeId scope) => + _log('[$scope] -> onScopeStartInitialize'); + + @override + void onScopeInitialized(ScopeId scope) => + _log('[$scope] -> onScopeInitialized'); + + @override + void onScopeInitializeFailed( + ScopeId scope, + Object exception, + StackTrace stackTrace, + ) => + _log('[$scope] -> onScopeInitializeFailed', exception, stackTrace); + + @override + void onScopeStartDispose(ScopeId scope) => + _log('[$scope] -> onScopeStartDispose'); + + @override + void onScopeDisposed(ScopeId scope) => _log('[$scope] -> onScopeDisposed'); + + @override + void onScopeDisposeDepFailed( + ScopeId scope, + DepId dep, + Object exception, + StackTrace stackTrace, + ) => + _log('[$scope] -> onScopeDisposeDepFailed', exception, stackTrace); + + @override + void onValueStartCreate(ScopeId scope, DepId dep) => + _log('[$scope.$dep] -> onValueStartCreate'); + + @override + void onValueCreated(ScopeId scope, DepId dep, ValueMeta? valueMeta) => + _log('[$scope.$dep] -> onValueCreated'); + + @override + void onValueCreateFailed( + ScopeId scope, + DepId dep, + Object exception, + StackTrace stackTrace, + ) => + _log('[$scope.$dep] -> onValueCreated', exception, stackTrace); + + @override + void onValueCleared(ScopeId scope, DepId dep, ValueMeta? valueMeta) => + _log('[$scope.$dep]($valueMeta) -> onValueCleared'); + + @override + void onDepDisposeFailed( + ScopeId scope, DepId dep, Object exception, StackTrace stackTrace) => + _log('[$scope.$dep] -> onDepDisposeFailed', exception, stackTrace); + + @override + void onDepDisposed(ScopeId scope, DepId dep) => + _log('[$scope.$dep] -> onDepDisposed'); + + @override + void onDepInitializeFailed( + ScopeId scope, DepId dep, Object exception, StackTrace stackTrace) => + _log('[$scope.$dep] -> onDepInitializeFailed', exception, stackTrace); + + @override + void onDepInitialized(ScopeId scope, DepId dep) => + _log('[$scope.$dep] -> onDepInitialized'); + + @override + void onDepStartDispose(ScopeId scope, DepId dep) => + _log('[$scope.$dep] -> onDepStartDispose'); + + @override + void onDepStartInitialize(ScopeId scope, DepId dep) => + _log('[$scope.$dep] -> onDepStartInitialize'); +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/example/pubspec.yaml b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/example/pubspec.yaml new file mode 100644 index 0000000..05be9b8 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/example/pubspec.yaml @@ -0,0 +1,14 @@ +name: yx_scope_example +description: "An example for yx_scope" +version: 1.0.0 +publish_to: 'none' + +environment: + sdk: ">=2.19.0 <4.0.0" + +dependencies: + yx_scope: + path: .. + +dev_dependencies: + lints: ^2.0.1 diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/lib/advanced.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/lib/advanced.dart new file mode 100644 index 0000000..ac69618 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/lib/advanced.dart @@ -0,0 +1,2 @@ +export 'src/monitoring/raw_observers.dart'; +export 'src/base_scope_container.dart' show CustomDep, CustomAsyncDep; diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/lib/src/base_scope_container.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/lib/src/base_scope_container.dart new file mode 100644 index 0000000..0430d75 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/lib/src/base_scope_container.dart @@ -0,0 +1,288 @@ +import 'dart:async'; +import 'dart:collection'; + +import 'package:meta/meta.dart'; + +import 'core/async_lifecycle.dart'; +import 'core/scope_exception.dart'; +import 'core/scope_state.dart'; +import 'monitoring/observers.dart'; +import 'monitoring/models/dep_id.dart'; +import 'monitoring/models/scope_id.dart'; +import 'monitoring/models/value_meta.dart'; +import 'monitoring/raw_observers.dart'; +import 'monitoring/scope_observatory_internal.dart'; +import 'scope_container.dart'; +import 'scope_state_streamable.dart'; + +part 'core/scope_state_holder.dart'; + +part 'core_scope_holder.dart'; + +part 'custom_dep.dart'; + +part 'dep.dart'; + +part 'monitoring/observers_internal.dart'; + +part 'monitoring/models/scope_meta.dart'; + +part 'scope_holder.dart'; + +part 'scope_module.dart'; + +part 'test_utils/scope_container_test_utils.dart'; + +part 'test_utils/scope_state_test_utils.dart'; + +typedef _AsyncVoidCallback = Future Function(); + +typedef _AsyncVoidCallbackRemove = void Function(); + +typedef AsyncDepCallback = Future Function(Value dep); + +/// {@template base_scope_container} +/// A container and a description of a scope. +/// +/// A [BaseScopeContainer] is responsible for managing the lifecycle +/// of its associated dependencies. +/// +/// It provides a convenient way to group +/// related dependencies together and initialize them at the same time. +/// +/// To create a [ScopeContainer]: +/// 1. Declare a subclass +/// 2. Declare dependencies: late final yourEntityDep = [dep] (or [asyncDep]) +/// 3. Override [initializeQueue] getter to specify which dependencies +/// should be initialized when the [ScopeContainer] is initialized. +/// +/// An example: +/// +/// class SomeScopeContainer extends ScopeContainer { +/// late final appManagerDep = dep(() => AppManager()); +/// +/// late final reporterDep = dep(() => Reporter()); +/// +/// late final navigationDep = dep(() => Navigation()); +/// } +/// {@endtemplate} +abstract class BaseScopeContainer extends Scope { + final _container = []; + + final String? _name; + late final ScopeId _id; + + late final DepObserverInternal _depObserver; + late final AsyncDepObserverInternal _asyncDepObserver; + + BaseScopeContainer({String? name}) + : _name = name, + super._() { + _id = ScopeId(runtimeType, hashCode, _name); + _depObserver = DepObserverInternal(this); + _asyncDepObserver = AsyncDepObserverInternal(this); + } + + /// A queue of the initialization for [AsyncDep]. + /// The order of the execution is the following: + /// [Dep]s inside each Set are executed in parallel in random order — + /// if dependencies do not depend from each other then place them in Set. + /// Sets are executed in the order of the List — from 0 to the next and so on. + /// If some dependencies must be executed in specific order, consider placing + /// them in separate Sets and order relatively. + @protected + List> get initializeQueue => []; + + /// The only correct way to declare a [Dep] inside [BaseScopeContainer]. + /// Returns a factory for your [Value]. + /// + /// Here is an example: + /// ``` + /// late final someManagerDep = dep(() => SomeManager()); + /// ``` + /// [Dep] must be a late final field inside [BaseScopeContainer]. No methods! + /// Add suffix 'Dep' to the name of your dependency — it explicitly says in the code + /// that you are working with [Dep] and not the entity (ex. SomeManager) itself. + /// + /// If your entity depends on the other entity, use this approach: + /// ``` + /// late final dependentManagerDep = dep(() => DependentManager(someManagerDep.get)); + /// ``` + /// In this example DependentManager depends on SomeManager. So we access + /// SomeManager via Dep and pass an actual instance + /// of the SomeManager inside DependentManager. + /// + /// [name] parameter can be ignored unless you want to see distinguish + /// dependencies with the same [Type] in the same or in different scopes. + @nonVirtual + @protected + Dep dep( + DepBuilder builder, { + String? name, + }) => + Dep._(this, builder, name: name, observer: _depObserver); + + /// Exactly the same as [BaseScopeContainer.dep] but you only allowed + /// to declare [AsyncLifecycle] dependencies using this method. + /// + /// [name] parameter can be ignored unless you want to see distinguish + /// dependencies with the same [Type] in the same or in different scopes. + @nonVirtual + @protected + AsyncDep asyncDep( + DepBuilder builder, { + String? name, + }) => + rawAsyncDep( + builder, + init: (value) => value.init(), + dispose: (value) => value.dispose(), + name: name, + ); + + /// Exactly the same as [BaseScopeContainer.asyncDep] but you must + /// declare init and dispose functions. + /// + /// It is useful if you don't want to implement [AsyncLifecycle] interface, + /// but you still have async init/dispose process in your entity. + /// + /// [name] parameter can be ignored unless you want to see distinguish + /// dependencies with the same [Type] in the same or in different scopes. + @nonVirtual + @protected + AsyncDep rawAsyncDep( + DepBuilder builder, { + required AsyncDepCallback init, + required AsyncDepCallback dispose, + String? name, + }) => + AsyncDep._( + this, + builder, + init: init, + dispose: dispose, + name: name, + observer: _asyncDepObserver, + ); + + void _registerDep(Dep dep) => _container.add(dep); + + void _unregister() { + for (final dep in _container.reversed) { + dep._unregister(); + } + _container.clear(); + } +} + +/// Mixin for [BaseScopeContainer]. +/// +/// {@template child_scope_container} +/// Helps to have a parent in a scope. +/// If the scope has a parent you can access +/// parent dependencies within a current scope. +/// {@endtemplate} +mixin ChildScopeContainerMixin on BaseScopeContainer { + Parent? _parent; + + /// Must not be used anyone except for the child with this mixin. + /// Should only be used inside constructor, see [ChildScopeContainer]. + @protected + set parent(Parent parent) => _parent = parent; + + /// Access parent of the current [BaseScopeContainer]. + Parent get parent { + final p = _parent; + if (p == null) { + throw ScopeException( + '$runtimeType: You must set parent in your Scope constructor', + ); + } + return p; + } +} + +/// Mixin for [BaseScopeContainer]. +/// {@template data_scope_container} +/// Helps to have a initial data in a scope. +/// If the scope has data, you can access +/// it as a non-nullable value within current scope. +/// {@endtemplate} +mixin DataScopeContainerMixin on BaseScopeContainer { + Data? _data; + + /// Must not be used anyone except for the child with this mixin. + /// Should only be used inside constructor, see [DataScopeContainer]. + @protected + set data(Data data) => _data = data; + + /// Access data of the current [BaseScopeContainer]. + Data get data { + final data = _data; + if (data == null) { + throw ScopeException( + '$runtimeType: You must set data in your Scope constructor', + ); + } + return data; + } +} + +/// This is an abstract interface for a custom developer interface +/// that is considered to be implemented by their [BaseScopeContainer]. +/// +/// For example, you have a [ScopeContainer]: +/// +/// class SomeScopeContainer extends ScopeContainer { +/// late final appManagerDep = dep(() => AppManager()); +/// +/// late final navigationDep = dep(() => Navigation()); +/// +/// late final reporterDep = dep(() => Reporter()); +/// } +/// +/// But you don't want to share actual deps with your consumers or +/// you don't want them to know anything about your SomeScopeContainer implementation. +/// +/// Then you create an interface: +/// +/// abstract class SomeScope implements BaseScope { +/// AppManager get appManager; +/// +/// NavigationManager get navigationManager; +/// } +/// +/// and implement it by your SomeScopeContainer: +/// +/// class SomeScopeContainer extends ScopeContainer implements SomeScope { +/// @override +/// AppManager get appManager => appManagerDep.get; +/// +/// @override +/// NavigationManager get navigationManager => navigationDep.get; +/// +/// late final appManagerDep = dep(() => AppManager()); +/// +/// late final navigationDep = dep(() => Navigation()); +/// +/// late final reporterDep = dep(() => Reporter()); +/// } +/// +/// It helps you in two ways: +/// 1. You can now use [BaseScopeHolder] (or Child/Data/ChildData-ScopeHolder) +/// and pass your interface as a first generic parameter for your ScopeHolder. +/// It narrows access to your deps giving only the contract that is declared by the interface. +/// 2. You can pass you interface as a [parent] to your [ChildScopeHolder] instead of +/// actual [BaseScopeContainer] implementation. It also narrows access to deps from the parent. +abstract class Scope { + final _disposeListeners = <_AsyncVoidCallback>{}; + + _AsyncVoidCallbackRemove _listenDispose(_AsyncVoidCallback listener) { + _disposeListeners.add(listener); + return () { + _disposeListeners.remove(listener); + }; + } + + Scope._(); +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/lib/src/core/async_lifecycle.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/lib/src/core/async_lifecycle.dart new file mode 100644 index 0000000..2d4a16d --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/lib/src/core/async_lifecycle.dart @@ -0,0 +1,18 @@ +/// An interface for entities that must be initialized and disposed. +/// You can use this interface both for asynchronous and synchronous initialization. +/// +/// If your entity implements this interface and it's created in [ScopeContainer], +/// you have to use [AsyncDep] for instance +/// and pass this dep into [ScopeContainer.initializeQueue]. +abstract class AsyncLifecycle { + /// When you use [AsyncLifecycle] in [ScopeContainer], + /// this method will be called during initialization of this [ScopeContainer]. + Future init(); + + /// When you use [AsyncLifecycle] in [ScopeContainer], + /// this method will be called during dispose of the [ScopeContainer]. + Future dispose(); + + /// AsyncLifecycle can be used only as an interface via 'implements' + const AsyncLifecycle._(); +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/lib/src/core/scope_exception.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/lib/src/core/scope_exception.dart new file mode 100644 index 0000000..8d8d71b --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/lib/src/core/scope_exception.dart @@ -0,0 +1,21 @@ +// This exception must be throw in cases when +// a consumer of this package uses it +// in some inappropriate way. +class ScopeException implements Exception { + final String message; + + const ScopeException(this.message); + + @override + String toString() => 'ScopeException: $message'; +} + +// This error must be throw in cases when +// the problem happens because of +// internal code problems of this package. +class ScopeError extends ScopeException { + const ScopeError(String message) : super(message); + + @override + String toString() => 'ScopeError: $message'; +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/lib/src/core/scope_state.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/lib/src/core/scope_state.dart new file mode 100644 index 0000000..ad81804 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/lib/src/core/scope_state.dart @@ -0,0 +1,46 @@ +abstract class ScopeState { + ScopeState._(); + + factory ScopeState.none() = ScopeStateNone; + factory ScopeState.initializing() = ScopeStateInitializing; + factory ScopeState.available({required Scope scope}) = ScopeStateAvailable; + factory ScopeState.disposing() = ScopeStateDisposing; + + bool get none => this is ScopeStateNone; + + bool get initializing => this is ScopeStateInitializing; + + bool get available => this is ScopeStateAvailable; + + bool get disposing => this is ScopeStateDisposing; +} + +class ScopeStateNone extends ScopeState { + ScopeStateNone() : super._(); + + @override + String toString() => 'ScopeState<$Scope>.none'; +} + +class ScopeStateInitializing extends ScopeState { + ScopeStateInitializing() : super._(); + + @override + String toString() => 'ScopeState<$Scope>.initializing'; +} + +class ScopeStateAvailable extends ScopeState { + final Scope scope; + + ScopeStateAvailable({required this.scope}) : super._(); + + @override + String toString() => 'ScopeState<$Scope>.available'; +} + +class ScopeStateDisposing extends ScopeState { + ScopeStateDisposing() : super._(); + + @override + String toString() => 'ScopeState<$Scope>.disposing'; +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/lib/src/core/scope_state_holder.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/lib/src/core/scope_state_holder.dart new file mode 100644 index 0000000..2e20352 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/lib/src/core/scope_state_holder.dart @@ -0,0 +1,181 @@ +part of '../base_scope_container.dart'; + +typedef StateListener = void Function(S? scope); + +typedef ScopeStateListener = void Function(ScopeState state); + +typedef RemoveStateListener = void Function(); + +class ScopeStateHolder { + final _listeners = LinkedList>(); + ScopeState _state; + + bool _debugCanAddListeners = true; + + ScopeStateHolder(this._state); + + Scope? get scope { + final state = this.state; + if (state is ScopeStateAvailable) { + return state.scope; + } + return null; + } + + ScopeState get state => _state; + + void _updateState(ScopeState state) { + _state = state; + + final errors = []; + final stackTraces = []; + + final listeners = [..._listeners]; + for (final listener in listeners) { + try { + listener.listener(state); + } on Object catch (e, s) { + errors.add(e); + stackTraces.add(s); + } + } + + if (errors.isNotEmpty) { + throw NotifyListenerError._(errors, stackTraces); + } + } + + /// Subscribes to the state. + /// + /// The [listener] callback will be called immediately on addition and + /// synchronously whenever [state] changes. + /// + /// Note: This method only calls the callback when the state is [ScopeStateAvailable] + /// (passing the scope object) or when the scope is [ScopeStateNone] (passing null). + /// It doesn't trigger for [ScopeStateInitializing] or [ScopeStateDisposing] states. + /// + /// Set [emitImmediately] to true if you want to an immediate execution + /// of the [listener] with the current state. + /// + /// To remove this [listener], call the function returned by [listen]. + /// + /// Listeners cannot add other listeners. + /// Adding and removing listeners has a constant time-complexity. + RemoveStateListener listen( + StateListener listener, { + bool emitImmediately = false, + }) => + _listen( + (state) { + if (state is ScopeStateAvailable) { + listener(state.scope); + } else if (state is ScopeStateNone) { + listener(null); + } + }, + emitImmediately: emitImmediately, + ); + + /// Subscribes to the state. + /// + /// The [listener] callback will be called immediately on addition and + /// synchronously whenever [state] changes. + /// + /// Note: This method emits on every [ScopeState] change. + /// + /// Set [emitImmediately] to true if you want to an immediate execution + /// of the [listener] with the current state. + /// + /// To remove this [listener], call the function returned by [listen]. + /// + /// Listeners cannot add other listeners. + /// Adding and removing listeners has a constant time-complexity. + RemoveStateListener listenState( + ScopeStateListener listener, { + bool emitImmediately = false, + }) => + _listen( + (state) => listener(state), + emitImmediately: emitImmediately, + ); + + RemoveStateListener _listen( + void Function(ScopeState state) listener, { + bool emitImmediately = false, + }) { + assert(() { + if (!_debugCanAddListeners) { + throw ConcurrentModificationError(); + } + return true; + }(), ''); + final listenerEntry = Entry(listener); + if (emitImmediately) { + assert(_debugSetCanAddListeners(false), ''); + try { + // Intentionally unsafe call of the listener before adding to the [_listeners] + // so that if there is an exception — we throw it back to consumer + // with an original stacktrace without adding to the [_listeners]. + listener(state); + } on Object catch (_) { + rethrow; + } finally { + assert(_debugSetCanAddListeners(true), ''); + } + } + _listeners.add(listenerEntry); + + return () { + if (listenerEntry.list != null) { + listenerEntry.unlink(); + } + }; + } + + bool _debugSetCanAddListeners(bool value) { + assert(() { + _debugCanAddListeners = value; + return true; + }(), ''); + return true; + } +} + +@visibleForTesting +class Entry extends LinkedListEntry> { + Entry(this.listener); + + final ScopeStateListener listener; +} + +/// An error thrown when tried to update the state of a [ScopeStateHolder], +/// but some of the listeners threw an exception. +class NotifyListenerError extends Error { + NotifyListenerError._( + this.errors, + this.stackTraces, + ) : assert( + errors.length == stackTraces.length, + 'errors and stackTraces must match', + ); + + final List errors; + final List stackTraces; + + @override + String toString() { + final buffer = StringBuffer(); + + for (var i = 0; i < errors.length; i++) { + final error = errors[i]; + final stackTrace = stackTraces[i]; + + buffer + ..writeln(error) + ..writeln(stackTrace); + } + + return 'Some of listeners threw exception when updating the state. ' + 'The exceptions thrown are:\n$buffer'; + } +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/lib/src/core_scope_holder.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/lib/src/core_scope_holder.dart new file mode 100644 index 0000000..e6ee29b --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/lib/src/core_scope_holder.dart @@ -0,0 +1,384 @@ +part of 'base_scope_container.dart'; + +/// DO NOT USE THIS CLASS MANUALLY! +/// Use [ScopeHolder], [ChildScopeHolder], +/// [DataScopeHolder] or [ChildDataScopeHolder] instead. +/// +/// Holder contains the state of a [BaseScopeContainer] — null or the scope itself. +/// This is the core entity that provides access to the [BaseScopeContainer]. +abstract class CoreScopeHolder + extends ScopeStateHolder with ScopeStateStreamable { + final ScopeObserverInternal _scopeObserverInternal; + final List? _depObservers; + final List? _asyncDepObservers; + + Completer? _waitLifecycleCompleter; + + CoreScopeHolder({ + List? scopeObservers, + List? depObservers, + List? asyncDepObservers, + // ignore: deprecated_member_use_from_same_package + @Deprecated('Use scopeObservers instead') + List? scopeListeners, + // ignore: deprecated_member_use_from_same_package + @Deprecated('Use depObservers instead') List? depListeners, + // ignore: deprecated_member_use_from_same_package + @Deprecated('Use asyncDepObservers instead') + List? asyncDepListeners, + }) : assert(!(scopeListeners != null && scopeObservers != null), + 'Both scopeObservers and scopeListeners passed as arguments to ScopeHolder. Consider using only scopeObservers'), + assert(!(depListeners != null && depObservers != null), + 'Both depObservers and depListeners passed as arguments to ScopeHolder. Consider using only depObservers'), + assert(!(asyncDepListeners != null && asyncDepObservers != null), + 'Both asyncDepObservers and asyncDepListeners passed as arguments to ScopeHolder. Consider using only asyncDepObservers'), + _scopeObserverInternal = + ScopeObserverInternal(scopeObservers ?? scopeListeners), + _depObservers = depObservers ?? depListeners, + _asyncDepObservers = asyncDepObservers ?? asyncDepListeners, + super(ScopeState.none()); + + /// Initialize scope. [Scope] becomes available and everyone can + /// start working with it via [BaseScopeHolder]. + /// + /// Throws [ScopeException] in following cases: + /// 1. [Container] does not implement [Scope] + /// 2. [init] is called when the previous [init] is still running + /// 3. [init] is called when [Scope] is already [ScopeState.available] + /// 4. [init] is called when [dispose] is now running + /// and there is already another [init] is waiting for [dispose] to complete + /// 5. [AsyncDep] from another [BaseScopeContainer] initialized in this [BaseScopeContainer.initializeQueue]. + /// + @protected + @mustCallSuper + Future init(Container scope) async { + if (scope is! Scope) { + throw ScopeException('You must implement $Scope for your $Container'); + } + + if (state.initializing) { + throw ScopeException( + 'You are trying to initialize $Container that is initializing right now. ' + 'Given instances of the $Container might be different, ' + 'so do not call `create` sequentially without `drops`', + ); + } + + if (state.available) { + throw ScopeException( + 'You are trying to initialize $Container that has been already initialized' + 'Given instances of the $Container might be different, ' + 'so do not call `create` sequentially without `drops`', + ); + } + + if (state.disposing) { + Logger.warning( + '$Container calls init method while disposing. ' + 'This is a weird situation and can lead to unexpected behaviour.', + ); + final currentCompleter = _waitLifecycleCompleter; + if (currentCompleter != null) { + throw ScopeException( + 'Scope is already waiting for dispose in order to be recreated again. ' + 'Probably you have called `create` method without await a few times in a row.', + ); + } + final completer = Completer.sync(); + _waitLifecycleCompleter = completer; + final removeListener = listenState((state) { + if (state.none) { + completer.complete(); + } else { + completer.completeError( + ScopeError( + 'Unexpected state ($state) after ${ScopeState.disposing},' + ' must be ${ScopeState.none}', + ), + ); + } + }); + try { + await completer.future; + } on Object catch (e, s) { + Error.throwWithStackTrace( + ScopeError( + '$e\n' + 'Unexpected exception when were waiting for dispose during initialization. ' + 'This is definitely an error in the library,' + ' please contact an owner, if you see this message.', + ), + s, + ); + } finally { + _waitLifecycleCompleter = null; + removeListener(); + Logger.debug( + 'Wait for scope dispose has completed, state=$state', + ); + if (!state.none) { + throw ScopeError( + 'Scope initialization waited for dispose of the previous scope state, ' + 'it\'s expected to be ${ScopeState.none}, ' + 'but it appeared to be $state.' + 'This is definitely an error in the library,' + ' please contact an owner, if you see this message.', + ); + } + } + } + + _prepareObservers(scope); + + _scopeObserverInternal.onScopeStartInitialize(scope); + + _initializing(); + + // Cache already initialized dependencies in order to + // dispose only initialized ones in case of an exception + final initialized = >[]; + final queue = scope.initializeQueue; + try { + for (var i = 0; i < queue.length; i++) { + initialized.add({}); + final depSet = queue[i]; + await Future.wait( + depSet.map( + (dep) { + final scopeType = Container; + final depType = dep.runtimeType; + if (dep._scope != scope) { + throw ScopeException( + 'You are initializing async dep $depType ' + 'within ${scope.runtimeType}#${scope.hashCode}, ' + 'but the dep declared in ${dep._scope.runtimeType}#${dep._scope.hashCode}', + ); + } + Logger.debug('($scopeType) Initializing: $depType'); + return dep._init().then( + (_) { + initialized[i].add(dep); + Logger.debug('($scopeType) Initialized: $depType'); + }, + ); + }, + ), + ); + } + } on Object catch (e, s) { + _scopeObserverInternal.onScopeInitializeFailed(scope, e, s); + + await _drop(initializedScope: scope, initializedDeps: initialized); + rethrow; + } + _available(scope as Scope); + _scopeObserverInternal.onScopeInitialized(scope); + } + + /// Dispose scope. [Scope] becomes unavailable. + /// + /// In debug throws [AssertionError] in following cases: + /// 1. [init] is called when the previous [dispose] is still running + /// 2. [init] is called when [Scope] is already [ScopeState.none] + /// + /// Throws [ScopeException] in following cases: + /// 1. [init] is called when [init] is now running + /// and there is already another [dispose] is waiting for [init] to complete + /// + @mustCallSuper + Future drop() => _drop(); + + Future _drop({ + Container? initializedScope, + List>? initializedDeps, + }) async { + if (!((initializedScope == null && initializedDeps == null) || + (initializedScope != null && initializedDeps != null))) { + throw const ScopeError( + '[Internal] _drop method must be called with either ' + 'both nullable args (user manual drop) or ' + 'both non-nullable args (drop during initialization)', + ); + } + + if (state.disposing) { + assert( + false, + 'You are trying to dispose $Container that is disposing right now', + ); + return; + } + + if (state.none) { + assert( + false, + 'You are trying to dispose $Container that has been already disposed or never existed', + ); + return; + } + + if (state.initializing) { + // if no initialized dependencies has been passed + // then this is a normal drop + if (initializedDeps == null && initializedScope == null) { + Logger.warning( + '$Container calls dispose method while initializing. ' + 'This is a weird situation and can lead to unexpected behaviour.', + ); + + final currentCompleter = _waitLifecycleCompleter; + if (currentCompleter != null) { + throw ScopeException( + 'Scope is already waiting for initialization in order to be disposed again. ' + 'Probably you have called `drop` method without await a few times in a row.', + ); + } + final completer = Completer.sync(); + _waitLifecycleCompleter = completer; + + final removeListener = listenState((state) { + if (state.available) { + completer.complete(); + } else { + completer.completeError( + ScopeError( + 'Unexpected state ($state) after ${ScopeState.initializing},' + ' must be ${ScopeState.available}', + ), + ); + } + }); + try { + await completer.future; + } on Object catch (e, s) { + Error.throwWithStackTrace( + ScopeError( + '$e\n' + 'Unexpected exception when were waiting for initialization during dispose. ' + 'This is definitely an error in the library,' + ' please contact an owner, if you see this message.', + ), + s, + ); + } finally { + _waitLifecycleCompleter = null; + removeListener(); + Logger.debug( + 'Wait for scope initialization has completed, state=$state', + ); + if (!state.available) { + throw ScopeError( + 'Scope dispose waited for initialization of the previous scope state, ' + 'it\'s expected to be ${ScopeState.available}, ' + 'but it appears to be $state. ' + 'This is definitely an error in the library,' + ' please contact an owner, if you see this message.', + ); + } + } + } else { + Logger.warning( + '$Container calls dispose method, because of some exception. ' + 'See stacktrace below for more details.', + ); + } + // If no initialized dependencies has been passed it means this is an internal drop. + // It happens only when some Exception appeared during init. + // In this case we do not wait for availability and do the drop. + } + + final scope = this.scope as Container? ?? initializedScope; + if (scope == null) { + throw ScopeError( + '$Container must not be null if scope state is $state', + ); + } + + _scopeObserverInternal.onScopeStartDispose(scope); + + _disposing(); + + Logger.debug('Dispose children'); + final listeners = [...scope._disposeListeners]; + await Future.wait(listeners.map((e) => e.call())); + + // If there were already initialized dependencies + // then we dispose only them and do not bother others + final queue = (initializedDeps ?? scope.initializeQueue).reversed; + for (final depSet in queue) { + await Future.wait( + depSet.map( + (dep) { + final scopeType = Container; + final depType = dep.runtimeType; + Logger.debug('($scopeType) Disposing: $depType'); + return dep + ._dispose() + .then((_) => Logger.debug('($scopeType) Disposed: $depType')) + // ignore: avoid_types_on_closure_parameters + .catchError((Object e, StackTrace s) { + Logger.error( + 'Exception happened during $depType dispose ($scopeType). ' + 'This dependency is skipped and dispose continued.', + e, + s, + ); + _scopeObserverInternal.onScopeDisposeDepFailed( + scope, + dep, + e, + s, + ); + // We continue disposing even in case of an exception + // because we have to complete scope dispose at least for + // dependencies without exceptions and for the scope itself. + }); + }, + ), + ); + } + scope._unregister(); + _clearObservers(scope); + await _disposed(); + _scopeObserverInternal.onScopeDisposed(scope); + } + + // ignore: use_setters_to_change_properties + void _updateScope(ScopeState state) { + Logger.debug('$Container state: $state'); + _updateState(state); + } + + void _initializing() => _updateScope(ScopeState.initializing()); + + Future _disposed() async { + try { + _updateScope(ScopeState.none()); // must be the first call in this method + } on NotifyListenerError catch (e, s) { + Logger.error('Some listeners thrown an exception during dispose', e, s); + } + } + + void _available(Scope scope) { + try { + _updateScope(ScopeState.available( + scope: scope)); // must be the first call in this method + } on NotifyListenerError catch (e, s) { + Logger.error('Some listeners thrown an exception during init', e, s); + } + } + + void _disposing() => _updateScope(ScopeState.disposing()); + + void _prepareObservers(Container scope) { + scope._depObserver._observers = _depObservers; + scope._asyncDepObserver._observers = _asyncDepObservers; + scope._asyncDepObserver._asyncDepObservers = _asyncDepObservers; + } + + void _clearObservers(Container scope) { + scope._depObserver._observers = null; + scope._asyncDepObserver._observers = null; + scope._asyncDepObserver._asyncDepObservers = null; + } +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/lib/src/custom_dep.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/lib/src/custom_dep.dart new file mode 100644 index 0000000..eb59e99 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/lib/src/custom_dep.dart @@ -0,0 +1,82 @@ +part of 'base_scope_container.dart'; + +/// Abstract base class for creating custom dependency types. +/// +/// Use this class as a base when you need to create a specialized dependency +/// with custom behavior, validation, or additional functionality beyond what +/// the standard [Dep] class provides. +/// +/// Example: +/// ```dart +/// class DatabaseDep extends CustomDep { +/// DatabaseDep(BaseScopeContainer scope, String connectionString) +/// : super(scope, () => Database.connect(connectionString)); +/// +/// @override +/// Database get get { +/// final db = super.get; +/// if (!db.isConnected) { +/// throw StateError('Database connection is not available'); +/// } +/// return db; +/// } +/// } +/// ``` +abstract class CustomDep extends Dep { + /// Creates a custom dependency. + /// + /// [scope] - The scope container that owns this dependency + /// [builder] - Factory function that creates the dependency value + /// [name] - Optional name for debugging purposes + /// [observer] - Optional observer for monitoring dependency lifecycle + CustomDep( + BaseScopeContainer scope, + DepBuilder builder, { + String? name, + DepObserverInternal? observer, + }) : super._(scope, builder, name: name, observer: observer); +} + +/// Abstract base class for creating custom async dependency types. +/// +/// Use this class as a base when you need to create a specialized async dependency +/// that requires initialization and disposal with custom behavior or validation. +/// +/// Example: +/// ```dart +/// class HttpClientDep extends CustomAsyncDep { +/// HttpClientDep(BaseScopeContainer scope, {required String baseUrl}) +/// : super( +/// scope, +/// () => HttpClient()..baseUrl = baseUrl, +/// init: (client) async { +/// await client.authenticate(); +/// print('HTTP client authenticated'); +/// }, +/// dispose: (client) async { +/// await client.logout(); +/// client.close(); +/// print('HTTP client disposed'); +/// }, +/// ); +/// } +/// ``` +abstract class CustomAsyncDep extends AsyncDep { + /// Creates a custom async dependency. + /// + /// [scope] - The scope container that owns this dependency + /// [builder] - Factory function that creates the dependency value + /// [init] - Async callback called during dependency initialization + /// [dispose] - Async callback called during dependency disposal + /// [name] - Optional name for debugging purposes + /// [observer] - Optional observer for monitoring dependency lifecycle + CustomAsyncDep( + BaseScopeContainer scope, + DepBuilder builder, { + required AsyncDepCallback init, + required AsyncDepCallback dispose, + String? name, + AsyncDepObserverInternal? observer, + }) : super._(scope, builder, + name: name, observer: observer, init: init, dispose: dispose); +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/lib/src/dep.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/lib/src/dep.dart new file mode 100644 index 0000000..1733f7c --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/lib/src/dep.dart @@ -0,0 +1,168 @@ +part of 'base_scope_container.dart'; + +/// A factory method that creates an entity in [Dep]. +typedef DepBuilder = Value Function(); + +/// Callback creates an entity based on current [BaseScopeContainer] +/// and will be used as a primary factory for creating an entity. +typedef OverrideDepBuilder = Value + Function(Container container); + +/// Extract meta information about [Dep], +/// e.x. DepId. +/// This extra class helps to stay the interface of +/// the main [Dep] clean an contain only dep.get method. +/// +/// This class must be used only locally and must not be +/// assigned to any field or global final or variable. +class DepMeta { + final Dep _dep; + + DepMeta(Dep dep) : _dep = dep; + + DepId get id => _dep._id; +} + +/// A description for a dependency. +/// Basically this is a factory class for any custom entity. +class Dep { + final String? _name; + + final BaseScopeContainer _scope; + final DepBuilder _builder; + final DepObserverInternal? _observer; + + late final DepId _id; + + _DepValue? _value; + + var _registered = false; + + Dep._( + this._scope, + this._builder, { + String? name, + DepObserverInternal? observer, + }) : _name = name, + _observer = observer { + _id = DepId(Value, hashCode, _name); + _scope._registerDep(this); + _registered = true; + } + + /// Returns an entity by request + Value get get { + if (!_registered) { + throw ScopeException( + 'You are trying to get an instance of $Value ' + 'from the Dep ${_name ?? hashCode.toString()}, ' + 'but the Scope ${_scope._name ?? _scope.hashCode.toString()} has been disposed. ' + 'Probably you stored an instance of the Dep ' + 'somewhere away from the Scope. ' + 'Do not keep a Dep instance separately from it\'s Scope, ' + 'and access Dep instance only directly from the Scope.', + ); + } + + final crtValue = _value; + if (crtValue != null) { + return crtValue.value; + } else { + try { + _observer?.onValueStartCreate(this); + final newValue = _builder(); + + _value = _DepValue(newValue); + _observer?.onValueCreated(this, newValue); + return newValue; + } on Object catch (e, s) { + _observer?.onValueCreateFailed(this, e, s); + rethrow; + } + } + } + + void _unregister() { + if (!_registered) { + throw ScopeError( + 'Dep._unregister() is called when it\'s not really registered yet — ' + 'this is definitely an error in the library, ' + 'please contact an owner, if you see this message.', + ); + } + final value = _value?.value; + _value = null; + _observer?.onValueCleared(this, value); + _registered = false; + } +} + +/// A description for a dependency that implements [AsyncLifecycle]. +/// This dependency will be initialized and disposed along with [BaseScopeContainer]. +class AsyncDep extends Dep { + final AsyncDepCallback _initCallback; + final AsyncDepCallback _disposeCallback; + + final AsyncDepObserverInternal? _asyncDepObserver; + + var _initialized = false; + + AsyncDep._( + BaseScopeContainer scope, + DepBuilder builder, { + required AsyncDepCallback init, + required AsyncDepCallback dispose, + String? name, + AsyncDepObserverInternal? observer, + }) : _initCallback = init, + _disposeCallback = dispose, + _asyncDepObserver = observer, + super._(scope, builder, name: name, observer: observer); + + Future _init() async { + final value = super.get; + try { + _asyncDepObserver?.onDepStartInitialize(this); + await _initCallback(value); + _initialized = true; + _asyncDepObserver?.onDepInitialized(this); + } on Object catch (e, s) { + _asyncDepObserver?.onDepInitializeFailed(this, e, s); + rethrow; + } + } + + Future _dispose() async { + assert( + _initialized, + 'Dispose of $runtimeType has been called without initialization', + ); + final value = get; + try { + _initialized = false; + _asyncDepObserver?.onDepStartDispose(this); + await _disposeCallback(value); + _asyncDepObserver?.onDepDisposed(this); + } on Object catch (e, s) { + _asyncDepObserver?.onDepDisposeFailed(this, e, s); + rethrow; + } + } + + @override + Value get get { + assert( + _initialized, + 'You have forgotten to add $runtimeType to initializeQueue or it ' + 'has been used before initialization by another dep. ' + 'Try to reorder deps in initializeQueue.', + ); + return super.get; + } +} + +class _DepValue { + final T value; + + const _DepValue(this.value); +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/lib/src/monitoring/listeners.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/lib/src/monitoring/listeners.dart new file mode 100644 index 0000000..14c0c98 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/lib/src/monitoring/listeners.dart @@ -0,0 +1,11 @@ +part of 'observers.dart'; + +@Deprecated('Use ScopeObserver instead') +abstract class ScopeListener implements ScopeObserver {} + +@Deprecated('Use DepObserver instead') +abstract class DepListener implements DepObserver {} + +@Deprecated('Use AsyncDepObserver instead') +abstract class AsyncDepListener extends DepListener + implements AsyncDepObserver {} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/lib/src/monitoring/models/dep_id.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/lib/src/monitoring/models/dep_id.dart new file mode 100644 index 0000000..a92aa7b --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/lib/src/monitoring/models/dep_id.dart @@ -0,0 +1,14 @@ +/// A ready-only unique identifier of a [Dep]. +/// An instance of DepId is always the same for +/// the same Dep instance. +class DepId { + final Type valueType; + final int depHashCode; + final String? name; + + const DepId(this.valueType, this.depHashCode, this.name); + + @override + String toString() => + '${name ?? valueType}${name != null ? '[$valueType]' : ''}[$depHashCode]'; +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/lib/src/monitoring/models/scope_id.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/lib/src/monitoring/models/scope_id.dart new file mode 100644 index 0000000..d5a3f16 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/lib/src/monitoring/models/scope_id.dart @@ -0,0 +1,14 @@ +/// A ready-only unique identifier of a [ScopeContainer]. +/// An instance of ScopeId is always the same for +/// the same [ScopeContainer] instance. +class ScopeId { + final Type type; + final int scopeHashCode; + final String? name; + + const ScopeId(this.type, this.scopeHashCode, this.name); + + @override + String toString() => + '${name ?? type}${name != null ? '[$type]' : ''}[$scopeHashCode]'; +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/lib/src/monitoring/models/scope_meta.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/lib/src/monitoring/models/scope_meta.dart new file mode 100644 index 0000000..ee30371 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/lib/src/monitoring/models/scope_meta.dart @@ -0,0 +1,16 @@ +part of '../../base_scope_container.dart'; + +/// Extract meta information about [BaseScopeContainer], +/// e.x. ScopeId. +/// This extra class helps to stay the interface of +/// the main [BaseScopeContainer] clean and contain only dep-related method. +/// +/// This class must be used only locally and must not be +/// assigned to any field or global final or variable. +class ScopeMeta { + final BaseScopeContainer _scope; + + const ScopeMeta(BaseScopeContainer scope) : _scope = scope; + + ScopeId get id => _scope._id; +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/lib/src/monitoring/models/value_meta.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/lib/src/monitoring/models/value_meta.dart new file mode 100644 index 0000000..d7f0ac0 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/lib/src/monitoring/models/value_meta.dart @@ -0,0 +1,13 @@ +/// A ready-only meta info of created value in a [Dep]. +class ValueMeta { + final Type valueType; + final int valueHashCode; + + const ValueMeta(this.valueType, this.valueHashCode); + + static ValueMeta? build(Object? object) => + object == null ? null : ValueMeta(object.runtimeType, object.hashCode); + + @override + String toString() => '$valueType[$valueHashCode]'; +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/lib/src/monitoring/observers.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/lib/src/monitoring/observers.dart new file mode 100644 index 0000000..5b56248 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/lib/src/monitoring/observers.dart @@ -0,0 +1,89 @@ +import 'models/dep_id.dart'; +import 'models/scope_id.dart'; +import 'models/value_meta.dart'; + +part 'listeners.dart'; + +/// Interface for observing [Scope] state. +/// +/// To observe [Scope] state create your implementation of this interface +/// and pass it to your [ScopeHolder]. +abstract class ScopeObserver { + void onScopeStartInitialize(ScopeId scope); + + void onScopeInitialized(ScopeId scope); + + void onScopeInitializeFailed( + ScopeId scope, + Object exception, + StackTrace stackTrace, + ); + + void onScopeStartDispose(ScopeId scope); + + void onScopeDisposed(ScopeId scope); + + /// The method is called when dispose of the [dep] has failed + /// during the dispose phase of the [scope]. + /// + /// This method can be called many times during dispose phase. + void onScopeDisposeDepFailed( + ScopeId scope, + DepId dep, + Object exception, + StackTrace stackTrace, + ); + + const ScopeObserver._(); +} + +/// Interface for observing [Dep] state. +/// +/// To observe [Dep] state create your implementation of this interface +/// and pass it to your [ScopeHolder]. +abstract class DepObserver { + void onValueStartCreate(ScopeId scope, DepId dep); + + void onValueCreated(ScopeId scope, DepId dep, ValueMeta? valueMeta); + + void onValueCreateFailed( + ScopeId scope, + DepId dep, + Object exception, + StackTrace stackTrace, + ); + + void onValueCleared(ScopeId scope, DepId dep, ValueMeta? valueMeta); + + const DepObserver._(); +} + +/// Interface for observing [AsyncDep] state. +/// +/// To observe [AsyncDep] state create your implementation of this interface +/// and pass it to your [ScopeHolder]. +abstract class AsyncDepObserver implements DepObserver { + void onDepStartInitialize(ScopeId scope, DepId dep); + + void onDepInitialized(ScopeId scope, DepId dep); + + void onDepStartDispose(ScopeId scope, DepId dep); + + void onDepDisposed(ScopeId scope, DepId dep); + + void onDepInitializeFailed( + ScopeId scope, + DepId dep, + Object exception, + StackTrace stackTrace, + ); + + void onDepDisposeFailed( + ScopeId scope, + DepId dep, + Object exception, + StackTrace stackTrace, + ); + + const AsyncDepObserver._(); +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/lib/src/monitoring/observers_internal.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/lib/src/monitoring/observers_internal.dart new file mode 100644 index 0000000..1487ad4 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/lib/src/monitoring/observers_internal.dart @@ -0,0 +1,299 @@ +part of '../base_scope_container.dart'; + +/// DO NOT USE THIS CLASS MANUALLY +/// +/// [RawScopeObserver] implementation for [CoreScopeHolder] that notifies +/// [ScopeObserver]s +class ScopeObserverInternal implements RawScopeObserver { + final List? _observers; + + const ScopeObserverInternal(this._observers); + + @override + void onScopeStartInitialize(BaseScopeContainer scope) { + RawScopeObserver.override?.safeNotify( + (listener) => listener.onScopeStartInitialize(scope), + ); + _safeNotifyAll( + _observers, + (listener) => listener.onScopeStartInitialize(scope._id), + ); + } + + @override + void onScopeInitialized(BaseScopeContainer scope) { + RawScopeObserver.override?.safeNotify( + (listener) => listener.onScopeInitialized(scope), + ); + _safeNotifyAll( + _observers, + (listener) => listener.onScopeInitialized(scope._id), + ); + } + + @override + void onScopeInitializeFailed( + BaseScopeContainer scope, + Object exception, + StackTrace stackTrace, + ) { + RawScopeObserver.override?.safeNotify( + (listener) => listener.onScopeInitializeFailed( + scope, + exception, + stackTrace, + ), + ); + _safeNotifyAll( + _observers, + (listener) => listener.onScopeInitializeFailed( + scope._id, + exception, + stackTrace, + ), + ); + } + + @override + void onScopeStartDispose(BaseScopeContainer scope) { + RawScopeObserver.override?.safeNotify( + (listener) => listener.onScopeStartDispose(scope), + ); + _safeNotifyAll( + _observers, + (listener) => listener.onScopeStartDispose(scope._id), + ); + } + + @override + void onScopeDisposed(BaseScopeContainer scope) { + RawScopeObserver.override?.safeNotify( + (listener) => listener.onScopeDisposed(scope), + ); + _safeNotifyAll( + _observers, + (listener) => listener.onScopeDisposed(scope._id), + ); + } + + @override + void onScopeDisposeDepFailed( + BaseScopeContainer scope, + Dep dep, + Object exception, + StackTrace stackTrace, + ) { + RawScopeObserver.override?.safeNotify( + (listener) => listener.onScopeDisposeDepFailed( + scope, + dep, + exception, + stackTrace, + ), + ); + _safeNotifyAll( + _observers, + (listener) => listener.onScopeDisposeDepFailed( + scope._id, + dep._id, + exception, + stackTrace, + ), + ); + } +} + +/// DO NOT USE THIS CLASS MANUALLY +/// +/// [RawDepObserver] implementation for [Dep] that notifies +/// [DepObserver]s +class DepObserverInternal { + final BaseScopeContainer _scope; + List? _observers; + + DepObserverInternal(this._scope); + + void onValueStartCreate(Dep dep) { + RawDepObserver.override?.safeNotify( + (listener) => listener.onValueStartCreate(_scope, dep), + ); + _safeNotifyAll( + _observers, + (listener) => listener.onValueStartCreate(_scope._id, dep._id), + ); + } + + void onValueCreated(Dep dep, Object? value) { + RawDepObserver.override?.safeNotify( + (listener) => listener.onValueCreated(_scope, dep, value), + ); + _safeNotifyAll( + _observers, + (listener) => listener.onValueCreated( + _scope._id, + dep._id, + ValueMeta.build(value), + ), + ); + } + + void onValueCreateFailed( + Dep dep, + Object exception, + StackTrace stackTrace, + ) { + RawDepObserver.override?.safeNotify( + (listener) => listener.onValueCreateFailed( + _scope, + dep, + exception, + stackTrace, + ), + ); + _safeNotifyAll( + _observers, + (listener) => listener.onValueCreateFailed( + _scope._id, + dep._id, + exception, + stackTrace, + ), + ); + } + + void onValueCleared(Dep dep, Object? value) { + RawDepObserver.override?.safeNotify( + (listener) => listener.onValueCleared(_scope, dep, value), + ); + _safeNotifyAll( + _observers, + (listener) => listener.onValueCleared( + _scope._id, + dep._id, + ValueMeta.build(value), + ), + ); + } +} + +/// DO NOT USE THIS CLASS MANUALLY +/// +/// [RawDepObserver] implementation for [AsyncDep] that notifies +/// [AsyncDepObserver]s +class AsyncDepObserverInternal extends DepObserverInternal { + List? _asyncDepObservers; + + AsyncDepObserverInternal(BaseScopeContainer scope) : super(scope); + + void onDepStartInitialize(Dep dep) { + RawAsyncDepObserver.override?.safeNotify( + (listener) => listener.onDepStartInitialize(_scope, dep), + ); + _safeNotifyAll( + _asyncDepObservers, + (listener) => listener.onDepStartInitialize(_scope._id, dep._id), + ); + } + + void onDepInitialized(Dep dep) { + RawAsyncDepObserver.override?.safeNotify( + (listener) => listener.onDepInitialized(_scope, dep), + ); + _safeNotifyAll( + _asyncDepObservers, + (listener) => listener.onDepInitialized(_scope._id, dep._id), + ); + } + + void onDepStartDispose(Dep dep) { + RawAsyncDepObserver.override?.safeNotify( + (listener) => listener.onDepStartDispose(_scope, dep), + ); + _safeNotifyAll( + _asyncDepObservers, + (listener) => listener.onDepStartDispose(_scope._id, dep._id), + ); + } + + void onDepDisposed(Dep dep) { + RawAsyncDepObserver.override?.safeNotify( + (listener) => listener.onDepDisposed(_scope, dep), + ); + _safeNotifyAll( + _asyncDepObservers, + (listener) => listener.onDepDisposed(_scope._id, dep._id), + ); + } + + void onDepInitializeFailed( + Dep dep, + Object exception, + StackTrace stackTrace, + ) { + RawAsyncDepObserver.override?.safeNotify( + (listener) => listener.onDepInitializeFailed( + _scope, + dep, + exception, + stackTrace, + ), + ); + _safeNotifyAll( + _asyncDepObservers, + (listener) => listener.onDepInitializeFailed( + _scope._id, + dep._id, + exception, + stackTrace, + ), + ); + } + + void onDepDisposeFailed( + Dep dep, + Object exception, + StackTrace stackTrace, + ) { + RawAsyncDepObserver.override?.safeNotify( + (listener) => listener.onDepDisposeFailed( + _scope, + dep, + exception, + stackTrace, + ), + ); + _safeNotifyAll( + _asyncDepObservers, + (listener) => listener.onDepDisposeFailed( + _scope._id, + dep._id, + exception, + stackTrace, + ), + ); + } +} + +extension _SafeNotification on T { + void safeNotify(void Function(T it) callback) { + try { + callback(this); + } on Object catch (e, s) { + Logger.warning( + 'An error occurred in a listener $T#$hashCode: $e\n$s', + ); + } + } +} + +void _safeNotifyAll( + List? listeners, + void Function(T listener) callback, +) { + if (listeners == null) { + return; + } + for (final listener in listeners) { + listener.safeNotify(callback); + } +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/lib/src/monitoring/raw_listeners.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/lib/src/monitoring/raw_listeners.dart new file mode 100644 index 0000000..2d18c2c --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/lib/src/monitoring/raw_listeners.dart @@ -0,0 +1,28 @@ +part of 'raw_observers.dart'; + +@Deprecated('Use RawScopeObserver instead') +abstract class RawScopeListener extends RawScopeObserver { + static set override(RawScopeObserver? override) => + RawScopeObserver.override = override; + + static RawScopeObserver? get override => RawScopeObserver.override; + + RawScopeListener._() : super._(); +} + +@Deprecated('Use RawDepObserver instead') +abstract class RawDepListener extends RawDepObserver { + static set override(RawDepObserver? override) => + RawDepObserver.override = override; + + static RawDepObserver? get override => RawDepObserver.override; +} + +@Deprecated('Use RawAsyncDepObserver instead') +abstract class RawAsyncDepListener extends RawAsyncDepObserver + implements RawDepListener { + static set override(RawAsyncDepObserver? override) => + RawAsyncDepObserver.override = override; + + static RawAsyncDepObserver? get override => RawAsyncDepObserver.override; +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/lib/src/monitoring/raw_observers.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/lib/src/monitoring/raw_observers.dart new file mode 100644 index 0000000..e17d865 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/lib/src/monitoring/raw_observers.dart @@ -0,0 +1,82 @@ +import '../base_scope_container.dart'; + +part 'raw_listeners.dart'; + +/// Consider using [ScopeObserver], [DepObserver] and [AsyncDepObserver] instead — these are Observers with a safe read-only access. +/// [RawScopeObserver], [RawDepObserver] and [RawAsyncDepObserver] are an advanced direct access for rare cases. +/// So if you are not sure if this is the right choice for you then it's probably not. +/// +/// [RawScopeObserver] is the Observer with a direct access to [BaseScopeContainer] and [Dep]. +abstract class RawScopeObserver { + static RawScopeObserver? override; + + RawScopeObserver._(); + + void onScopeStartInitialize(BaseScopeContainer scope); + + void onScopeInitialized(BaseScopeContainer scope); + + void onScopeInitializeFailed( + BaseScopeContainer scope, + Object exception, + StackTrace stackTrace, + ); + + void onScopeStartDispose(BaseScopeContainer scope); + + void onScopeDisposed(BaseScopeContainer scope); + + void onScopeDisposeDepFailed( + BaseScopeContainer scope, + Dep dep, + Object exception, + StackTrace stackTrace, + ); +} + +/// [RawDepObserver] is the Observer with a direct access to [BaseScopeContainer], [Dep] and created instance. +/// More details in [RawScopeObserver] +abstract class RawDepObserver { + static RawDepObserver? override; + + void onValueStartCreate(BaseScopeContainer scope, Dep dep); + + void onValueCreated(BaseScopeContainer scope, Dep dep, Object? value); + + void onValueCreateFailed( + BaseScopeContainer scope, + Dep dep, + Object exception, + StackTrace stackTrace, + ); + + void onValueCleared(BaseScopeContainer scope, Dep dep, Object? value); +} + +/// [RawAsyncDepObserver] is the Observer with a direct access to [BaseScopeContainer] and [Dep]. +/// More details in [RawScopeObserver] +abstract class RawAsyncDepObserver implements RawDepObserver { + static RawAsyncDepObserver? override; + + void onDepStartInitialize(BaseScopeContainer scope, Dep dep); + + void onDepInitialized(BaseScopeContainer scope, Dep dep); + + void onDepStartDispose(BaseScopeContainer scope, Dep dep); + + void onDepDisposed(BaseScopeContainer scope, Dep dep); + + void onDepInitializeFailed( + BaseScopeContainer scope, + Dep dep, + Object exception, + StackTrace stackTrace, + ); + + void onDepDisposeFailed( + BaseScopeContainer scope, + Dep dep, + Object exception, + StackTrace stackTrace, + ); +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/lib/src/monitoring/scope_observatory.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/lib/src/monitoring/scope_observatory.dart new file mode 100644 index 0000000..7619cfc --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/lib/src/monitoring/scope_observatory.dart @@ -0,0 +1,30 @@ +/// Class for diagnostic purposes. +/// +/// Logging, state of scopes, info about dependencies. +class ScopeObservatory { + // Set this logger in order to log event in scopes. + // By default it logs nothing. + static ScopeLogger logger = const ScopeLogger(); + + const ScopeObservatory._(); +} + +class ScopeLogger { + const ScopeLogger(); + + void log( + LogType type, + Object message, { + Object? exception, + StackTrace? stackTrace, + }) { + // Override in order to log event in scopes + } +} + +enum LogType { + debug, + info, + warning, + error, +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/lib/src/monitoring/scope_observatory_internal.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/lib/src/monitoring/scope_observatory_internal.dart new file mode 100644 index 0000000..845f917 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/lib/src/monitoring/scope_observatory_internal.dart @@ -0,0 +1,16 @@ +import 'scope_observatory.dart'; + +class Logger { + static void debug(Object message) => + ScopeObservatory.logger.log(LogType.debug, message); + + static void info(Object message) => + ScopeObservatory.logger.log(LogType.info, message); + + static void warning(Object message) => + ScopeObservatory.logger.log(LogType.warning, message); + + static void error(Object message, Object exception, StackTrace stackTrace) => + ScopeObservatory.logger.log(LogType.error, message, + exception: exception, stackTrace: stackTrace); +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/lib/src/scope_container.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/lib/src/scope_container.dart new file mode 100644 index 0000000..f3a870d --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/lib/src/scope_container.dart @@ -0,0 +1,74 @@ +import 'base_scope_container.dart'; + +/// Root [BaseScopeContainer] without parent scope. +/// +/// {@macro base_scope_container} +abstract class ScopeContainer extends BaseScopeContainer { + ScopeContainer({String? name}) : super(name: name) { + _initializeQueueNoDuplications(this); + } +} + +/// {@macro child_scope_container} +abstract class ChildScopeContainer + extends BaseScopeContainer with ChildScopeContainerMixin { + ChildScopeContainer({ + required Parent parent, + String? name, + }) : super(name: name) { + this.parent = parent; + _initializeQueueNoDuplications(this); + } +} + +/// {@macro data_scope_container} +abstract class DataScopeContainer + extends BaseScopeContainer with DataScopeContainerMixin { + DataScopeContainer({ + required Data data, + String? name, + }) : super(name: name) { + this.data = data; + _initializeQueueNoDuplications(this); + } +} + +/// Combines [ChildScopeContainer] and [DataScopeContainer]. +/// +/// [ChildScopeContainer]: +/// {@macro child_scope_container} +/// +/// [DataScopeContainer]: +/// {@macro data_scope_container} +abstract class ChildDataScopeContainer extends BaseScopeContainer + with ChildScopeContainerMixin, DataScopeContainerMixin { + ChildDataScopeContainer({ + required Parent parent, + required Data data, + String? name, + }) : super(name: name) { + this.data = data; + this.parent = parent; + _initializeQueueNoDuplications(this); + } +} + +/// Checks if no duplicated async dependency instances +/// added into initializeQueue. +void _initializeQueueNoDuplications( + Container scope) { + // ignore: invalid_use_of_protected_member + final deps = scope.initializeQueue.expand((depSet) => depSet); + final counter = {}; + for (final dep in deps) { + counter[dep] = (counter[dep] ?? 0) + 1; + } + final duplications = counter.entries.where((entry) => entry.value > 1); + assert( + duplications.isEmpty, + '(${scope.runtimeType}) Following async dependencies has been added ' + 'to initializeQueue multiple times: ' + '[${duplications.map((e) => '${e.key.runtimeType}: ${e.value}').join(', ')}]', + ); +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/lib/src/scope_holder.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/lib/src/scope_holder.dart new file mode 100644 index 0000000..162ca04 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/lib/src/scope_holder.dart @@ -0,0 +1,456 @@ +part of 'base_scope_container.dart'; + +/// Simple holder for creating [BaseScopeHolder]. +/// +/// Here is an example: +/// ``` +/// class SomeScopeHolder extends ScopeHolder {} +/// ``` +/// You also have to declare SomeScopeContainer, see [BaseScopeContainer]. +/// +/// Now you have to create an instance of this holder and use it +/// to create/dispose scope and to communicate with it: +/// ``` +/// final someScopeHolder = SomeScopeHolder(); +/// +/// await someScopeHolder.create(); +/// +/// final scope = someScopeHolder.scope; +/// // Now an instance of a scope is available +/// print(someScopeHolder.scope?.runtimeType); // SomeScopeContainer +/// +/// // Access [ScopeContainer] only locally. Do not store it in fields. +/// if(scope != null) { +/// scope.appManager.someMethod(); +/// } +/// +/// await someScopeHolder.drop(); +/// // Now scope is not available, so it's null +/// print(someScopeHolder.scope?.runtimeType); // null +/// +/// // You can also subscribe to changes of a scope +/// someScopeHolder.listen((scope) { +/// if(scope != null) { +/// // scope exists and can be used +/// } else { +/// // scope is not available and disposed +/// } +/// } +/// ``` +/// +/// If you need to differentiate [BaseScopeContainer] and it's abstract interface +/// then you better use [BaseScopeHolder] directly. +abstract class ScopeHolder + extends BaseScopeHolder { + ScopeHolder({ + @Deprecated('Use scopeObservers instead') + List? scopeListeners, + @Deprecated('Use depObservers instead') List? depListeners, + @Deprecated('Use asyncDepObservers instead') + List? asyncDepListeners, + List? scopeObservers, + List? depObservers, + List? asyncDepObservers, + }) : super( + scopeObservers: scopeObservers, + depObservers: depObservers, + asyncDepObservers: asyncDepObservers, + // ignore: deprecated_member_use_from_same_package + scopeListeners: scopeListeners, + // ignore: deprecated_member_use_from_same_package + depListeners: depListeners, + // ignore: deprecated_member_use_from_same_package + asyncDepListeners: asyncDepListeners, + ); +} + +/// Simple holder for creating [BaseChildScopeHolder]. +/// +/// Here is an example: +/// ``` +/// class DependentScopeHolder +/// extends ChildScopeHolder { +/// DependentScopeHolder(SomeScopeContainer parent) : super(parent); +/// +/// @override +/// DependentScopeContainer createContainer(SomeScopeContainer parent) => +/// DependentScopeContainer( +/// parent: parent, +/// ); +/// } +/// ``` +/// You also have to declare DependentScopeContainer, see [BaseScopeContainer]. +/// +/// As long as you [ScopeContainer] has parent, you must declare this holder +/// inside parent [ScopeContainer]. Here is an example: +/// +/// class SomeScopeContainer extends ScopeContainer { +/// +/// // It must be late final as any [Dep] and you pass current [BaseScopeContainer] +/// // as a parent inside your DependentScopeHolder. +/// late final dependentScopeHolderDep = dep(() => DependentScopeHolder(this)); +/// +/// // ... +/// } +/// +/// If you need to differentiate [BaseScopeContainer] and it's abstract interface +/// then you better use [BaseChildScopeHolder] directly. +abstract class ChildScopeHolder, + Parent extends Scope> + extends BaseChildScopeHolder { + ChildScopeHolder( + Parent parent, { + @Deprecated('Use scopeObservers instead') + List? scopeListeners, + @Deprecated('Use depObservers instead') List? depListeners, + @Deprecated('Use asyncDepObservers instead') + List? asyncDepListeners, + List? scopeObservers, + List? depObservers, + List? asyncDepObservers, + }) : super( + parent, + scopeObservers: scopeObservers, + depObservers: depObservers, + asyncDepObservers: asyncDepObservers, + // ignore: deprecated_member_use_from_same_package + scopeListeners: scopeListeners, + // ignore: deprecated_member_use_from_same_package + depListeners: depListeners, + // ignore: deprecated_member_use_from_same_package + asyncDepListeners: asyncDepListeners, + ); +} + +/// Simple holder for creating [BaseDataScopeHolder]. +/// +/// Here is an example: +/// ``` +/// class DataScopeHolder extends DataScopeHolder { +/// @override +/// DataScopeContainer createContainer(Data data) => DataScopeContainer(data: data); +/// } +/// ``` +/// You also have to declare DataScopeContainer, see [BaseScopeContainer]. +/// +/// Working with scope: +/// ``` +/// await dataScopeHolder.create(SomeData()); +/// // Scope exists here +/// +/// await dataScopeHolder.drop(); +/// // Scope does not exist here +/// ``` +/// +/// If you need to differentiate [BaseScopeContainer] and it's abstract interface +/// then you better use [BaseDataScopeHolder] directly. +abstract class DataScopeHolder, + Data extends Object> + extends BaseDataScopeHolder { + DataScopeHolder({ + @Deprecated('Use scopeObservers instead') + List? scopeListeners, + @Deprecated('Use depObservers instead') List? depListeners, + @Deprecated('Use asyncDepObservers instead') + List? asyncDepListeners, + List? scopeObservers, + List? depObservers, + List? asyncDepObservers, + }) : super( + scopeObservers: scopeObservers, + depObservers: depObservers, + asyncDepObservers: asyncDepObservers, + // ignore: deprecated_member_use_from_same_package + scopeListeners: scopeListeners, + // ignore: deprecated_member_use_from_same_package + depListeners: depListeners, + // ignore: deprecated_member_use_from_same_package + asyncDepListeners: asyncDepListeners, + ); +} + +/// Simple holder for creating [BaseChildDataScopeHolder]. +/// +/// Here is an example: +/// ``` +/// class DataDependentScopeHolder +/// extends ChildDataScopeHolder { +/// DataDependentScopeHolder(SomeScopeContainer parent) : super(parent); +/// +/// @override +/// DataDependentScopeContainer createContainer(SomeScopeContainer parent, SomeData data) => +/// DataDependentScopeContainer( +/// data: data, +/// parent: parent, +/// ); +/// } +/// ``` +/// You also have to declare DataDependentScopeContainer, see [BaseScopeContainer]. +/// +/// As long as you [ScopeContainer] has parent, you must declare this holder +/// inside parent [ScopeContainer]. Here is an example: +/// +/// class SomeScopeContainer extends ScopeContainer { +/// +/// // It must be late final as any [Dep] and you pass current [BaseScopeContainer] +/// // as a parent inside your DependentScopeHolder. +/// late final dataDependentScopeHolderDep = dep(() => DataDependentScopeHolder(this)); +/// +/// // ... +/// } +/// +/// If you need to differentiate [BaseScopeContainer] and it's abstract interface +/// then you better use [BaseChildDataScopeHolder] directly. +abstract class ChildDataScopeHolder< + Container extends ChildDataScopeContainer, + Parent extends Scope, + Data extends Object> + extends BaseChildDataScopeHolder { + ChildDataScopeHolder( + Parent parent, { + @Deprecated('Use scopeObservers instead') + List? scopeListeners, + @Deprecated('Use depObservers instead') List? depListeners, + @Deprecated('Use asyncDepObservers instead') + List? asyncDepListeners, + List? scopeObservers, + List? depObservers, + List? asyncDepObservers, + }) : super( + parent, + scopeObservers: scopeObservers, + depObservers: depObservers, + asyncDepObservers: asyncDepObservers, + // ignore: deprecated_member_use_from_same_package + scopeListeners: scopeListeners, + // ignore: deprecated_member_use_from_same_package + depListeners: depListeners, + // ignore: deprecated_member_use_from_same_package + asyncDepListeners: asyncDepListeners, + ); +} + +/// Holder contains the state of a [BaseScopeContainer] — null or the scope itself. +/// This is the core entity that provides access to the [BaseScopeContainer]. +/// +/// This holder allows to keep a [BaseScopeContainer] as an abstract interface [Scope]. +/// Your [BaseScopeContainer] have to implement this [Scope] interface. +/// +/// You can use [ScopeHolder] if you don't need +/// to differentiate [BaseScopeContainer] and it's interface. +abstract class BaseScopeHolder + extends CoreScopeHolder { + BaseScopeHolder({ + @Deprecated('Use scopeObservers instead') + List? scopeListeners, + @Deprecated('Use depObservers instead') List? depListeners, + @Deprecated('Use asyncDepObservers instead') + List? asyncDepListeners, + List? scopeObservers, + List? depObservers, + List? asyncDepObservers, + }) : super( + scopeObservers: scopeObservers, + depObservers: depObservers, + asyncDepObservers: asyncDepObservers, + // ignore: deprecated_member_use_from_same_package + scopeListeners: scopeListeners, + // ignore: deprecated_member_use_from_same_package + depListeners: depListeners, + // ignore: deprecated_member_use_from_same_package + asyncDepListeners: asyncDepListeners, + ); + + @protected + Container createContainer(); + + Future create() => init(createContainer()); +} + +/// Holder contains the state of a [ChildScopeContainer] — null or the scope itself. +/// This is the core entity that provides access to the [ChildScopeContainer]. +/// +/// In order to create [BaseChildScopeHolder] +/// you have to provider a Parent [BaseScopeContainer]. +/// +/// This holder allows to keep a [BaseScopeContainer] as an abstract interface [Scope]. +/// Your [BaseScopeContainer] have to implement this [Scope] interface. +/// +/// You can use [ChildScopeHolder] if you don't need +/// to differentiate [BaseScopeContainer] and it's interface. +abstract class BaseChildScopeHolder< + ScopeType, + Container extends ChildScopeContainer, + Parent extends Scope> extends CoreScopeHolder + with _BaseChildScopeHolderMixin { + BaseChildScopeHolder( + Parent parent, { + @Deprecated('Use scopeObservers instead') + List? scopeListeners, + @Deprecated('Use depObservers instead') List? depListeners, + @Deprecated('Use asyncDepObservers instead') + List? asyncDepListeners, + List? scopeObservers, + List? depObservers, + List? asyncDepObservers, + }) : super( + scopeObservers: scopeObservers, + depObservers: depObservers, + asyncDepObservers: asyncDepObservers, + // ignore: deprecated_member_use_from_same_package + scopeListeners: scopeListeners, + // ignore: deprecated_member_use_from_same_package + depListeners: depListeners, + // ignore: deprecated_member_use_from_same_package + asyncDepListeners: asyncDepListeners, + ) { + this.parent = parent; + } + + @protected + Container createContainer(Parent parent); + + Future create() => init(createContainer(parent)); +} + +/// Holder contains the state of a [DataScopeContainer] — null or the scope itself. +/// This is the core entity that provides access to the [DataScopeContainer]. +/// +/// This holder allows to keep a [BaseScopeContainer] as an abstract interface [Scope]. +/// Your [BaseScopeContainer] have to implement this [Scope] interface. +/// +/// You can use [DataScopeHolder] if you don't need +/// to differentiate [BaseScopeContainer] and it's interface. +abstract class BaseDataScopeHolder< + Scope, + Container extends DataScopeContainer, + Data extends Object> extends CoreScopeHolder + with _BaseDataScopeHolderMixin { + BaseDataScopeHolder({ + @Deprecated('Use scopeObservers instead') + List? scopeListeners, + @Deprecated('Use depObservers instead') List? depListeners, + @Deprecated('Use asyncDepObservers instead') + List? asyncDepListeners, + List? scopeObservers, + List? depObservers, + List? asyncDepObservers, + }) : super( + scopeObservers: scopeObservers, + depObservers: depObservers, + asyncDepObservers: asyncDepObservers, + // ignore: deprecated_member_use_from_same_package + scopeListeners: scopeListeners, + // ignore: deprecated_member_use_from_same_package + depListeners: depListeners, + // ignore: deprecated_member_use_from_same_package + asyncDepListeners: asyncDepListeners, + ); + + @protected + Container createContainer(Data data); + + Future create(Data data) => init(createContainer(data)); +} + +/// Holder contains the state of a [ChildDataScopeContainer] — null or the scope itself. +/// This is the core entity that provides access to the [ChildDataScopeContainer]. +/// +/// In order to create [BaseChildDataScopeHolder] +/// you have to provider a Parent [BaseScopeContainer]. +/// +/// This holder allows to keep a [BaseScopeContainer] as an abstract interface [Scope]. +/// Your [BaseScopeContainer] have to implement this [Scope] interface. +/// +/// You can use [ChildDataScopeHolder] if you don't need +/// to differentiate [BaseScopeContainer] and it's interface. +abstract class BaseChildDataScopeHolder< + ScopeType, + Container extends ChildDataScopeContainer, + Parent extends Scope, + Data extends Object> extends CoreScopeHolder + with + _BaseChildScopeHolderMixin, + _BaseDataScopeHolderMixin { + BaseChildDataScopeHolder( + Parent parent, { + @Deprecated('Use scopeObservers instead') + List? scopeListeners, + @Deprecated('Use depObservers instead') List? depListeners, + @Deprecated('Use asyncDepObservers instead') + List? asyncDepListeners, + List? scopeObservers, + List? depObservers, + List? asyncDepObservers, + }) : super( + scopeObservers: scopeObservers, + depObservers: depObservers, + asyncDepObservers: asyncDepObservers, + // ignore: deprecated_member_use_from_same_package + scopeListeners: scopeListeners, + // ignore: deprecated_member_use_from_same_package + depListeners: depListeners, + // ignore: deprecated_member_use_from_same_package + asyncDepListeners: asyncDepListeners, + ) { + this.parent = parent; + } + + @protected + Container createContainer(Parent parent, Data data); + + Future create(Data data) => init(createContainer(parent, data)); +} + +mixin _BaseChildScopeHolderMixin< + ScopeType, + Container extends ChildScopeContainerMixin, + Parent extends Scope> on CoreScopeHolder { + Parent? _parent; + + /// Must not be used anyone except for the child with this mixin. + /// Should only be used inside constructor, see [ChildScopeContainer]. + @protected + set parent(Parent parent) => _parent = parent; + + /// Access parent of the current [BaseScopeContainer]. + Parent get parent { + final p = _parent; + if (p == null) { + throw ScopeException( + '$runtimeType: You must set parent in your ScopeHolder constructor', + ); + } + return p; + } + + _AsyncVoidCallbackRemove? _parentRemoveListener; + + @protected + @override + Future init(Container scope) async { + await super.init(scope); + _parentRemoveListener = parent._listenDispose(_onParentDropped); + } + + @override + Future drop() async { + _parentRemoveListener?.call(); + _parentRemoveListener = null; + await super.drop(); + } + + Future _onParentDropped() async { + /// When parent is dropped, we are sure + /// that current child scope won't be (and should no be) used again. + /// It's not true for dropping only the current scope, + /// that's why it's not in just drop method. + _listeners.clear(); + await drop(); + } +} + +mixin _BaseDataScopeHolderMixin< + Scope, + Container extends DataScopeContainerMixin, + Data extends Object> on CoreScopeHolder {} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/lib/src/scope_module.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/lib/src/scope_module.dart new file mode 100644 index 0000000..5cc4472 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/lib/src/scope_module.dart @@ -0,0 +1,60 @@ +part of 'base_scope_container.dart'; + +/// This class helps to decompose [ScopeContainer] into a number of features. +/// +/// For example we want to extract all monitoring entities into a separate feature: +/// +/// class SomeScopeContainer extends ScopeContainer { +/// // We declare [ScopeModule] inside it's [ScopeContainer] +/// late final monitorScopeModule = MonitorScopeModule(this); +/// +/// late final appManagerDep = dep(() => AppManager()); +/// +/// late final navigationDep = dep(() => Navigation()); +/// } +/// +/// class MonitorScopeModule extends ScopeModule { +/// MonitorScopeModule(super.container); +/// +/// late final reporterDep = dep(() => Reporter()); +/// +/// late final loggerDep = dep(() => Logger()); +/// } +abstract class ScopeModule { + final Container container; + + const ScopeModule(this.container); + + @protected + Dep dep( + DepBuilder builder, { + String? name, + }) => + container.dep(builder, name: name); + + @protected + AsyncDep asyncDep( + DepBuilder builder, { + String? name, + }) => + container.rawAsyncDep( + builder, + init: (value) => value.init(), + dispose: (value) => value.dispose(), + name: name, + ); + + @protected + AsyncDep rawAsyncDep( + DepBuilder builder, { + required AsyncDepCallback init, + required AsyncDepCallback dispose, + String? name, + }) => + container.rawAsyncDep( + builder, + init: init, + dispose: dispose, + name: name, + ); +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/lib/src/scope_state_streamable.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/lib/src/scope_state_streamable.dart new file mode 100644 index 0000000..1501d7c --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/lib/src/scope_state_streamable.dart @@ -0,0 +1,52 @@ +import 'dart:async'; + +import 'base_scope_container.dart'; +import 'core/scope_state.dart'; + +mixin ScopeStateStreamable on ScopeStateHolder { + Stream get stream { + late StreamController controller; + late RemoveStateListener removeStateListener; + + void onListen() { + removeStateListener = listen((scope) { + controller.add(scope); + }); + } + + void onCancel() async { + removeStateListener(); + await controller.close(); + } + + controller = StreamController( + onListen: onListen, + onCancel: onCancel, + ); + + return controller.stream; + } + + Stream> get stateStream { + late StreamController> controller; + late RemoveStateListener removeStateListener; + + void onListen() { + removeStateListener = listenState((state) { + controller.add(state); + }); + } + + void onCancel() async { + removeStateListener(); + await controller.close(); + } + + controller = StreamController>( + onListen: onListen, + onCancel: onCancel, + ); + + return controller.stream; + } +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/lib/src/test_utils/scope_container_test_utils.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/lib/src/test_utils/scope_container_test_utils.dart new file mode 100644 index 0000000..3cdac11 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/lib/src/test_utils/scope_container_test_utils.dart @@ -0,0 +1,8 @@ +part of '../base_scope_container.dart'; + +class ScopeContainerTestUtils { + /// Count of mounted providers. After dispose provider will be unmount. + @visibleForTesting + static int getDepCount(BaseScopeContainer container) => + container._container.length; +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/lib/src/test_utils/scope_state_test_utils.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/lib/src/test_utils/scope_state_test_utils.dart new file mode 100644 index 0000000..2e069e4 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/lib/src/test_utils/scope_state_test_utils.dart @@ -0,0 +1,12 @@ +part of '../base_scope_container.dart'; + +/// The class that has access to private fields of the given [ScopeStateHolder] +@visibleForTesting +class TestableScopeStateHolder { + final ScopeStateHolder holder; + + @visibleForTesting + const TestableScopeStateHolder(this.holder); + + LinkedList> get listeners => holder._listeners; +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/lib/yx_scope.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/lib/yx_scope.dart new file mode 100644 index 0000000..1df9fbc --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/lib/yx_scope.dart @@ -0,0 +1,20 @@ +library yx_scope; + +export 'src/base_scope_container.dart' + hide + ScopeObserverInternal, + DepObserverInternal, + AsyncDepObserverInternal, + Entry, + TestableScopeStateHolder, + CustomDep, + CustomAsyncDep; +export 'src/core/async_lifecycle.dart'; +export 'src/core/scope_exception.dart'; +export 'src/core/scope_state.dart'; +export 'src/monitoring/observers.dart'; +export 'src/monitoring/models/dep_id.dart'; +export 'src/monitoring/models/scope_id.dart'; +export 'src/monitoring/models/value_meta.dart'; +export 'src/monitoring/scope_observatory.dart'; +export 'src/scope_container.dart'; diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/pubspec.yaml b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/pubspec.yaml new file mode 100644 index 0000000..c9c2c0b --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/pubspec.yaml @@ -0,0 +1,24 @@ +name: yx_scope +description: A core package of the compile-safe DI framework with advanced scoping capabilities +version: 1.1.3 +repository: https://github.com/yandex/city-services-pub/tree/main/yx_scope/packages/yx_scope +issue_tracker: https://github.com/yandex/city-services-pub/issues +topics: + - di + - scope + - dependency-injection + - dependency-management + +environment: + sdk: ">=2.19.0 <4.0.0" + +dependencies: + meta: ^1.8.0 + +dev_dependencies: + test: ^1.16.0 + lints: ^2.0.1 + +screenshots: + - description: The yx_scope package logo + path: screenshots/yx_scope_logo.webp diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/screenshots/yx_scope_logo.webp b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/screenshots/yx_scope_logo.webp new file mode 100644 index 0000000..74536ea Binary files /dev/null and b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/screenshots/yx_scope_logo.webp differ diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/test/dep_test.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/test/dep_test.dart new file mode 100644 index 0000000..58bf285 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/test/dep_test.dart @@ -0,0 +1,349 @@ +import 'package:test/test.dart'; +import 'package:yx_scope/advanced.dart'; +import 'package:yx_scope/src/monitoring/raw_observers.dart'; +import 'package:yx_scope/yx_scope.dart'; + +import 'utils/test_logger.dart'; +import 'utils/utils.dart'; + +void main() { + final observer = _TestObserver(); + + setUp(() { + _TestDep.instances.clear(); + _TestAsyncDepNoAsyncLifecycle.instances.clear(); + _TestAsyncDep.instances.clear(); + ScopeObservatory.logger = const TestLogger(); + + RawScopeObserver.override = observer; + RawDepObserver.override = observer; + RawAsyncDepObserver.override = observer; + }); + + tearDown(() { + observer.scopeDeps.clear(); + RawScopeObserver.override = null; + RawDepObserver.override = null; + RawAsyncDepObserver.override = null; + }); + + group('sync dependencies', () { + test('dep is created lazily only when dep.get is called', () { + final scope = _TestScope(); + expect(_TestDep.instances.isEmpty, isTrue); + + final dep = scope.myDep; + expect(_TestDep.instances.isEmpty, isTrue); + + final myDep = dep.get; + expect(_TestDep.instances.length, 1); + // dep.get returns the same instance as was created + expect(_TestDep.instances.first, myDep); + }); + + test('dep creates only one instance', () { + final scope = _TestScope(); + expect(scope.myDep.get, scope.myDep.get); + }); + + test('dep is registered in observer lazily', () { + final scope = _TestScope(); + expect(observer.scopeDeps['_TestScope'], isNull); + + final dep = scope.myDep.get; + expect( + observer.scopeDeps['_TestScope']?.first, + dep.runtimeType.toString(), + ); + }); + + test( + 'dependencies with the same type but different names' + ' is registered separately in observer', () { + final scope = _TestScope(); + expect(observer.scopeDeps['_TestScope'], isNull); + + final dep1 = scope.myDep.get; + expect( + observer.scopeContainsDep('_TestScope', dep1.runtimeType.toString()), + isTrue, + ); + + final dep2 = scope.myDuplicatedDep.get; + expect( + observer.scopeContainsDep('_TestScope', dep2.runtimeType.toString()), + isTrue, + ); + }); + + test('dep returns non-nullable value', () { + final scope = _TestScope(); + expect(observer.scopeDeps['_TestScope'], isNull); + + final dep = scope.myNullableDep.get; + expect( + dep, + isNotNull, + ); + expect( + observer.scopeDeps['_TestScope']?.first + .startsWith(dep.runtimeType.toString()), + isTrue, + ); + }); + + test('dep returns null value', () { + final scope = _TestScope(isNullableDep: true); + expect(observer.scopeDeps['_TestScope'], isNull); + + final dep = scope.myNullableDep.get; + expect( + dep, + isNull, + ); + }); + }); + + group('async dependencies', () { + test( + 'async dep initialized lazily on scope init and disposed on scope dispose', + () async { + final scopeHolder = _TestAsyncDepScopeHolder(); + expect(_TestAsyncDepNoAsyncLifecycle.instances.isEmpty, isTrue); + expect(_TestAsyncDep.instances.isEmpty, isTrue); + + await scopeHolder.create(); + final scope = scopeHolder.scope; + if (scope == null) { + throw Exception('Scope must be no-null here'); + } + + final asyncDep = scope.myAsyncDep; + // no need to call dep.get, because it will be called during scope initialization + expect(_TestAsyncDepNoAsyncLifecycle.instances.length, 1); + + final secondAsyncDep = scope.mySecondAsyncDep; + // no need to call dep.get, because it will be called during scope initialization + expect(_TestAsyncDep.instances.length, 1); + + final depInstance = asyncDep.get; + expect(_TestAsyncDepNoAsyncLifecycle.instances.first, depInstance); + expect(depInstance._loaded, isTrue); + + final secondDepInstance = secondAsyncDep.get; + expect(_TestAsyncDep.instances.first, secondDepInstance); + expect(secondDepInstance._initialized, isTrue); + + // async dep creates only one instance + expect(depInstance, asyncDep.get); + expect(asyncDep.get, asyncDep.get); + expect(secondDepInstance, secondAsyncDep.get); + expect(secondAsyncDep.get, secondAsyncDep.get); + + await scopeHolder.drop(); + + expect(depInstance._loaded, isFalse); + expect(secondDepInstance._initialized, isFalse); + }); + + test('uninitialized async dep throws an assertion when calling dep.get', + () async { + final scopeHolder = _TestAsyncDepScopeHolder(); + expect(_TestAsyncDep.instances.isEmpty, isTrue); + + await scopeHolder.create(); + final scope = scopeHolder.scope; + if (scope == null) { + throw Exception('Scope must be no-null here'); + } + + final uninitializedAsyncDep = scope.uninitializedAsyncDep; + // We have 2 _TestAsyncDep deps in the scope + // (mySecondAsyncDep and uninitializedAsyncDep), + // but we forgotten to initialize this dep, + // so there is only one instance after scope creation. + expect(_TestAsyncDep.instances.length, 1); + + await expectAssertion(() => uninitializedAsyncDep.get); + + await scopeHolder.drop(); + }); + }); +} + +class _TestScope extends ScopeContainer { + final bool isNullableDep; + + _TestScope({this.isNullableDep = false}); + + late final myDep = dep(() => _TestDep()); + + late final myDuplicatedDep = dep(() => _TestDep(), name: 'duplicate'); + + late final myNullableDep = dep(() => isNullableDep ? null : _TestDep()); +} + +class _TestDep { + static final instances = <_TestDep>[]; + + _TestDep() { + instances.add(this); + } +} + +class _TestAsyncDepNoAsyncLifecycle { + static final instances = <_TestAsyncDepNoAsyncLifecycle>[]; + + _TestAsyncDepNoAsyncLifecycle() { + instances.add(this); + } + + var _loaded = false; + + Future load() async { + _loaded = true; + } + + void clear() { + _loaded = false; + } +} + +class _TestAsyncDep implements AsyncLifecycle { + static final instances = <_TestAsyncDep>[]; + + _TestAsyncDep() { + instances.add(this); + } + + var _initialized = false; + + @override + Future init() async { + _initialized = true; + } + + @override + Future dispose() async { + _initialized = false; + } +} + +class _TestAsyncDepScope extends ScopeContainer { + @override + List> get initializeQueue => [ + {myAsyncDep, mySecondAsyncDep} + ]; + + late final myAsyncDep = rawAsyncDep( + () => _TestAsyncDepNoAsyncLifecycle(), + init: (dep) async => dep.load(), + dispose: (dep) async => dep.clear(), + ); + + late final mySecondAsyncDep = asyncDep(() => _TestAsyncDep()); + + late final uninitializedAsyncDep = asyncDep( + () => _TestAsyncDep(), + name: 'uninitialized', + ); +} + +class _TestAsyncDepScopeHolder extends ScopeHolder<_TestAsyncDepScope> { + @override + _TestAsyncDepScope createContainer() => _TestAsyncDepScope(); +} + +class _TestObserver implements RawScopeObserver, RawAsyncDepObserver { + final scopeDeps = >{}; + + _TestObserver(); + + @override + void onDepDisposeFailed( + BaseScopeContainer scope, + Dep dep, + Object exception, + StackTrace stackTrace, + ) {} + + @override + void onDepDisposed(BaseScopeContainer scope, Dep dep) {} + + @override + void onDepInitializeFailed( + BaseScopeContainer scope, + Dep dep, + Object exception, + StackTrace stackTrace, + ) {} + + @override + void onDepInitialized(BaseScopeContainer scope, Dep dep) {} + + @override + void onDepStartDispose(BaseScopeContainer scope, Dep dep) {} + + @override + void onDepStartInitialize(BaseScopeContainer scope, Dep dep) {} + + @override + void onScopeDisposeDepFailed( + BaseScopeContainer scope, + Dep dep, + Object exception, + StackTrace stackTrace, + ) {} + + @override + void onScopeDisposed(BaseScopeContainer scope) => + scopeDeps.remove(ScopeMeta(scope).id.type.toString()); + + @override + void onScopeInitializeFailed( + BaseScopeContainer scope, + Object exception, + StackTrace stackTrace, + ) {} + + @override + void onScopeInitialized(BaseScopeContainer scope) {} + + @override + void onScopeStartDispose(BaseScopeContainer scope) {} + + @override + void onScopeStartInitialize(BaseScopeContainer scope) { + scopeDeps[ScopeMeta(scope).id.type.toString()] = {}; + } + + @override + void onValueCreateFailed( + BaseScopeContainer scope, + Dep dep, + Object exception, + StackTrace stackTrace, + ) {} + + @override + void onValueStartCreate(BaseScopeContainer scope, Dep dep) {} + + @override + void onValueCreated( + BaseScopeContainer scope, Dep dep, Object? value) { + final scopeMeta = ScopeMeta(scope); + final depMeta = DepMeta(dep); + final deps = scopeDeps[scopeMeta.id.type.toString()] ?? {}; + scopeDeps[scopeMeta.id.type.toString()] = deps; + deps.add(depMeta.id.valueType.toString()); + } + + @override + void onValueCleared( + BaseScopeContainer scope, Dep dep, Object? value) => + scopeDeps[ScopeMeta(scope).id.type.toString()]! + .remove(DepMeta(dep).id.valueType.toString()); + + bool scopeContainsDep(String scopeType, String depType) => + scopeDeps[scopeType]?.contains(depType) ?? false; +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/test/deprecated_listeners_test.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/test/deprecated_listeners_test.dart new file mode 100644 index 0000000..45ac1c4 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/test/deprecated_listeners_test.dart @@ -0,0 +1,65 @@ +import 'package:test/test.dart'; +import 'package:yx_scope/yx_scope.dart'; + +class AppScopeContainer extends ScopeContainer {} + +class AppScopeHolder extends ScopeHolder { + AppScopeHolder({ + // ignore: deprecated_member_use_from_same_package + List? scopeListeners, + // ignore: deprecated_member_use_from_same_package + List? depListeners, + // ignore: deprecated_member_use_from_same_package + List? asyncDepListeners, + List? scopeObservers, + List? depObservers, + List? asyncDepObservers, + }) : super( + // ignore: deprecated_member_use_from_same_package + scopeListeners: scopeListeners, + scopeObservers: scopeObservers, + // ignore: deprecated_member_use_from_same_package + depListeners: depListeners, + depObservers: depObservers, + // ignore: deprecated_member_use_from_same_package + asyncDepListeners: asyncDepListeners, + asyncDepObservers: asyncDepObservers, + ); + @override + AppScopeContainer createContainer() => AppScopeContainer(); +} + +void main() { + test( + 'Fail assert if both scopeListeners and scopeObservers passed to ScopeHolder', + () { + expect( + () => AppScopeHolder(scopeListeners: [], scopeObservers: []), + throwsA( + isA(), + ), + ); + }); + + test( + 'Fail assert if both depListeners and depObservers passed to ScopeHolder', + () { + expect( + () => AppScopeHolder(depListeners: [], depObservers: []), + throwsA( + isA(), + ), + ); + }); + + test( + 'Fail assert if both asyncDepListeners and asyncDepObservers passed to ScopeHolder', + () { + expect( + () => AppScopeHolder(asyncDepListeners: [], asyncDepObservers: []), + throwsA( + isA(), + ), + ); + }); +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/test/observers_test.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/test/observers_test.dart new file mode 100644 index 0000000..442902a --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/test/observers_test.dart @@ -0,0 +1,738 @@ +import 'dart:async'; + +import 'package:test/test.dart'; +import 'package:yx_scope/yx_scope.dart'; + +import 'utils/test_logger.dart'; + +void main() { + setUp(() { + ScopeObservatory.logger = const TestLogger(); + }); + + test('success calls in correct order with correct params', () async { + final listener = TestObserver(); + final holder = _TestScopeHolder(listener); + + await holder.create(); + + holder.scope?.syncDep.get; + + await holder.drop(); + + final events = listener._events; + expect(events[0].name, 'onScopeStartInitialize'); + expect(events[0].scope.type, _TestScopeContainer); + + expect(events[1].name, 'onValueStartCreate'); + expect(events[1].scope.type, _TestScopeContainer); + expect(events[1].dep?.valueType, _TestAsyncDep); + expect(events[1].scope.scopeHashCode, events[0].scope.scopeHashCode); + + expect(events[2].name, 'onValueCreated'); + expect(events[2].scope.type, _TestScopeContainer); + expect(events[2].dep?.valueType, _TestAsyncDep); + expect(events[2].dep?.depHashCode, events[1].dep?.depHashCode); + expect(events[2].scope.scopeHashCode, events[0].scope.scopeHashCode); + + expect(events[3].name, 'onDepStartInitialize'); + expect(events[3].scope.type, _TestScopeContainer); + expect(events[3].dep?.valueType, _TestAsyncDep); + expect(events[3].dep?.depHashCode, events[1].dep?.depHashCode); + expect(events[3].scope.scopeHashCode, events[0].scope.scopeHashCode); + + expect(events[4].name, 'onDepInitialized'); + expect(events[4].scope.type, _TestScopeContainer); + expect(events[4].dep?.valueType, _TestAsyncDep); + expect(events[4].dep?.depHashCode, events[1].dep?.depHashCode); + expect(events[4].scope.scopeHashCode, events[0].scope.scopeHashCode); + + expect(events[5].name, 'onScopeInitialized'); + expect(events[5].scope.type, _TestScopeContainer); + expect(events[5].scope.scopeHashCode, events[0].scope.scopeHashCode); + + expect(events[6].name, 'onValueStartCreate'); + expect(events[6].scope.type, _TestScopeContainer); + expect(events[6].dep?.valueType, _SyncDep); + expect(events[6].scope.scopeHashCode, events[0].scope.scopeHashCode); + + expect(events[7].name, 'onValueCreated'); + expect(events[7].scope.type, _TestScopeContainer); + expect(events[7].dep?.valueType, _SyncDep); + expect(events[7].dep?.depHashCode, events[6].dep?.depHashCode); + expect(events[7].scope.scopeHashCode, events[0].scope.scopeHashCode); + + expect(events[8].name, 'onScopeStartDispose'); + expect(events[8].scope.type, _TestScopeContainer); + expect(events[8].scope.scopeHashCode, events[0].scope.scopeHashCode); + + expect(events[9].name, 'onDepStartDispose'); + expect(events[9].scope.type, _TestScopeContainer); + expect(events[9].dep?.valueType, _TestAsyncDep); + expect(events[9].dep?.depHashCode, events[1].dep?.depHashCode); + expect(events[9].scope.scopeHashCode, events[0].scope.scopeHashCode); + + expect(events[10].name, 'onDepDisposed'); + expect(events[10].scope.type, _TestScopeContainer); + expect(events[10].dep?.valueType, _TestAsyncDep); + expect(events[10].dep?.depHashCode, events[1].dep?.depHashCode); + expect(events[10].scope.scopeHashCode, events[0].scope.scopeHashCode); + + expect(events[11].name, 'onValueCleared'); + expect(events[11].scope.type, _TestScopeContainer); + expect(events[11].dep?.valueType, _SyncDep); + expect(events[11].dep?.depHashCode, events[6].dep?.depHashCode); + expect(events[11].scope.scopeHashCode, events[0].scope.scopeHashCode); + + expect(events[12].name, 'onValueCleared'); + expect(events[12].scope.type, _TestScopeContainer); + expect(events[12].dep?.valueType, _TestAsyncDep); + expect(events[12].dep?.depHashCode, events[1].dep?.depHashCode); + expect(events[12].scope.scopeHashCode, events[0].scope.scopeHashCode); + + expect(events[13].name, 'onScopeDisposed'); + expect(events[13].scope.type, _TestScopeContainer); + expect(events[13].scope.scopeHashCode, events[0].scope.scopeHashCode); + + expect(events.length, 14); + }); + + test('init failure calls in correct order with correct params', () async { + final listener = TestObserver(); + final holder = _BrokenAsyncDepScopeHolder(listener); + + try { + await holder.create(); + } catch (e) { + expect(e, isA()); + } + + final events = listener._events; + expect(events[0].name, 'onScopeStartInitialize'); + expect(events[0].scope.type, _BrokenAsyncDepScopeContainer); + + expect(events[1].name, 'onValueStartCreate'); + expect(events[1].scope.type, _BrokenAsyncDepScopeContainer); + expect(events[1].dep?.valueType, _TestAsyncDep); + expect(events[1].scope.scopeHashCode, events[0].scope.scopeHashCode); + + expect(events[2].name, 'onValueCreated'); + expect(events[2].scope.type, _BrokenAsyncDepScopeContainer); + expect(events[2].dep?.valueType, _TestAsyncDep); + expect(events[2].dep?.depHashCode, events[1].dep?.depHashCode); + expect(events[2].scope.scopeHashCode, events[0].scope.scopeHashCode); + + expect(events[3].name, 'onDepStartInitialize'); + expect(events[3].scope.type, _BrokenAsyncDepScopeContainer); + expect(events[3].dep?.valueType, _TestAsyncDep); + expect(events[3].dep?.depHashCode, events[1].dep?.depHashCode); + expect(events[3].scope.scopeHashCode, events[0].scope.scopeHashCode); + + expect(events[4].name, 'onDepInitialized'); + expect(events[4].scope.type, _BrokenAsyncDepScopeContainer); + expect(events[4].dep?.valueType, _TestAsyncDep); + expect(events[4].dep?.depHashCode, events[1].dep?.depHashCode); + expect(events[4].scope.scopeHashCode, events[0].scope.scopeHashCode); + + expect(events[5].name, 'onValueStartCreate'); + expect(events[5].scope.type, _BrokenAsyncDepScopeContainer); + expect(events[5].dep?.valueType, _BrokenInitAsyncDep); + expect(events[5].scope.scopeHashCode, events[0].scope.scopeHashCode); + + expect(events[6].name, 'onValueCreated'); + expect(events[6].scope.type, _BrokenAsyncDepScopeContainer); + expect(events[6].dep?.valueType, _BrokenInitAsyncDep); + expect(events[6].dep?.depHashCode, events[5].dep?.depHashCode); + expect(events[6].scope.scopeHashCode, events[0].scope.scopeHashCode); + + expect(events[7].name, 'onDepStartInitialize'); + expect(events[7].scope.type, _BrokenAsyncDepScopeContainer); + expect(events[7].dep?.valueType, _BrokenInitAsyncDep); + expect(events[7].dep?.depHashCode, events[5].dep?.depHashCode); + expect(events[7].scope.scopeHashCode, events[0].scope.scopeHashCode); + + expect(events[8].name, 'onDepInitializeFailed'); + expect(events[8].scope.type, _BrokenAsyncDepScopeContainer); + expect(events[8].dep?.valueType, _BrokenInitAsyncDep); + expect(events[8].dep?.depHashCode, events[5].dep?.depHashCode); + expect(events[8].scope.scopeHashCode, events[0].scope.scopeHashCode); + expect(events[8].exception, isA()); + + expect(events[9].name, 'onScopeInitializeFailed'); + expect(events[9].scope.type, _BrokenAsyncDepScopeContainer); + expect(events[9].scope.scopeHashCode, events[0].scope.scopeHashCode); + expect(events[9].exception, isA()); + + expect(events[10].name, 'onScopeStartDispose'); + expect(events[10].scope.type, _BrokenAsyncDepScopeContainer); + expect(events[10].scope.scopeHashCode, events[0].scope.scopeHashCode); + + expect(events[11].name, 'onDepStartDispose'); + expect(events[11].scope.type, _BrokenAsyncDepScopeContainer); + expect(events[11].dep?.valueType, _TestAsyncDep); + expect(events[11].dep?.depHashCode, events[1].dep?.depHashCode); + expect(events[11].scope.scopeHashCode, events[0].scope.scopeHashCode); + + expect(events[12].name, 'onDepDisposed'); + expect(events[12].scope.type, _BrokenAsyncDepScopeContainer); + expect(events[12].dep?.valueType, _TestAsyncDep); + expect(events[12].dep?.depHashCode, events[1].dep?.depHashCode); + expect(events[12].scope.scopeHashCode, events[0].scope.scopeHashCode); + + expect(events[13].name, 'onValueCleared'); + expect(events[13].scope.type, _BrokenAsyncDepScopeContainer); + expect(events[13].dep?.valueType, _BrokenDisposeAsyncDep); + expect(events[13].scope.scopeHashCode, events[0].scope.scopeHashCode); + + expect(events[14].name, 'onValueCleared'); + expect(events[14].scope.type, _BrokenAsyncDepScopeContainer); + expect(events[14].dep?.valueType, _BrokenInitAsyncDep); + expect(events[14].dep?.depHashCode, events[5].dep?.depHashCode); + expect(events[14].scope.scopeHashCode, events[0].scope.scopeHashCode); + + expect(events[15].name, 'onValueCleared'); + expect(events[15].scope.type, _BrokenAsyncDepScopeContainer); + expect(events[15].dep?.valueType, _TestAsyncDep); + expect(events[15].dep?.depHashCode, events[1].dep?.depHashCode); + expect(events[15].scope.scopeHashCode, events[0].scope.scopeHashCode); + + expect(events[16].name, 'onScopeDisposed'); + expect(events[16].scope.type, _BrokenAsyncDepScopeContainer); + expect(events[16].scope.scopeHashCode, events[0].scope.scopeHashCode); + + expect(events.length, 17); + }); + + test('dispose calls in correct order with correct params', () async { + final listener = TestObserver(); + final holder = _BrokenAsyncDepScopeHolder(listener, checkDispose: true); + + await holder.create(); + try { + await holder.drop(); + } catch (_) { + fail('Drop must complete anyway'); + } + + final events = listener._events; + expect(events[0].name, 'onScopeStartInitialize'); + expect(events[0].scope.type, _BrokenAsyncDepScopeContainer); + + expect(events[1].name, 'onValueStartCreate'); + expect(events[1].scope.type, _BrokenAsyncDepScopeContainer); + expect(events[1].dep?.valueType, _TestAsyncDep); + expect(events[1].scope.scopeHashCode, events[0].scope.scopeHashCode); + + expect(events[2].name, 'onValueCreated'); + expect(events[2].scope.type, _BrokenAsyncDepScopeContainer); + expect(events[2].dep?.valueType, _TestAsyncDep); + expect(events[2].dep?.depHashCode, events[1].dep?.depHashCode); + expect(events[2].scope.scopeHashCode, events[0].scope.scopeHashCode); + + expect(events[3].name, 'onDepStartInitialize'); + expect(events[3].scope.type, _BrokenAsyncDepScopeContainer); + expect(events[3].dep?.valueType, _TestAsyncDep); + expect(events[3].dep?.depHashCode, events[1].dep?.depHashCode); + expect(events[3].scope.scopeHashCode, events[0].scope.scopeHashCode); + + expect(events[4].name, 'onDepInitialized'); + expect(events[4].scope.type, _BrokenAsyncDepScopeContainer); + expect(events[4].dep?.valueType, _TestAsyncDep); + expect(events[4].dep?.depHashCode, events[1].dep?.depHashCode); + expect(events[4].scope.scopeHashCode, events[0].scope.scopeHashCode); + + expect(events[5].name, 'onValueStartCreate'); + expect(events[5].scope.type, _BrokenAsyncDepScopeContainer); + expect(events[5].dep?.valueType, _BrokenDisposeAsyncDep); + expect(events[5].scope.scopeHashCode, events[0].scope.scopeHashCode); + + expect(events[6].name, 'onValueCreated'); + expect(events[6].scope.type, _BrokenAsyncDepScopeContainer); + expect(events[6].dep?.valueType, _BrokenDisposeAsyncDep); + expect(events[6].dep?.depHashCode, events[5].dep?.depHashCode); + expect(events[6].scope.scopeHashCode, events[0].scope.scopeHashCode); + + expect(events[7].name, 'onDepStartInitialize'); + expect(events[7].scope.type, _BrokenAsyncDepScopeContainer); + expect(events[7].dep?.valueType, _BrokenDisposeAsyncDep); + expect(events[7].dep?.depHashCode, events[5].dep?.depHashCode); + expect(events[7].scope.scopeHashCode, events[0].scope.scopeHashCode); + + expect(events[8].name, 'onDepInitialized'); + expect(events[8].scope.type, _BrokenAsyncDepScopeContainer); + expect(events[8].dep?.valueType, _BrokenDisposeAsyncDep); + expect(events[8].dep?.depHashCode, events[5].dep?.depHashCode); + expect(events[8].scope.scopeHashCode, events[0].scope.scopeHashCode); + + expect(events[9].name, 'onScopeInitialized'); + expect(events[9].scope.type, _BrokenAsyncDepScopeContainer); + expect(events[9].scope.scopeHashCode, events[0].scope.scopeHashCode); + + expect(events[10].name, 'onScopeStartDispose'); + expect(events[10].scope.type, _BrokenAsyncDepScopeContainer); + expect(events[10].scope.scopeHashCode, events[0].scope.scopeHashCode); + + expect(events[11].name, 'onDepStartDispose'); + expect(events[11].scope.type, _BrokenAsyncDepScopeContainer); + expect(events[11].dep?.valueType, _BrokenDisposeAsyncDep); + expect(events[11].dep?.depHashCode, events[5].dep?.depHashCode); + expect(events[11].scope.scopeHashCode, events[0].scope.scopeHashCode); + + expect(events[12].name, 'onDepDisposeFailed'); + expect(events[12].scope.type, _BrokenAsyncDepScopeContainer); + expect(events[12].dep?.valueType, _BrokenDisposeAsyncDep); + expect(events[12].dep?.depHashCode, events[5].dep?.depHashCode); + expect(events[12].scope.scopeHashCode, events[0].scope.scopeHashCode); + expect(events[12].exception, isA()); + + expect(events[13].name, 'onScopeDisposeDepFailed'); + expect(events[13].scope.type, _BrokenAsyncDepScopeContainer); + expect(events[13].dep?.valueType, _BrokenDisposeAsyncDep); + expect(events[13].dep?.depHashCode, events[5].dep?.depHashCode); + expect(events[13].scope.scopeHashCode, events[0].scope.scopeHashCode); + expect(events[13].exception, isA()); + + expect(events[14].name, 'onDepStartDispose'); + expect(events[14].scope.type, _BrokenAsyncDepScopeContainer); + expect(events[14].dep?.valueType, _TestAsyncDep); + expect(events[14].dep?.depHashCode, events[1].dep?.depHashCode); + expect(events[14].scope.scopeHashCode, events[0].scope.scopeHashCode); + + expect(events[15].name, 'onDepDisposed'); + expect(events[15].scope.type, _BrokenAsyncDepScopeContainer); + expect(events[15].dep?.valueType, _TestAsyncDep); + expect(events[15].dep?.depHashCode, events[1].dep?.depHashCode); + expect(events[15].scope.scopeHashCode, events[0].scope.scopeHashCode); + + expect(events[16].name, 'onValueCleared'); + expect(events[16].scope.type, _BrokenAsyncDepScopeContainer); + expect(events[16].dep?.valueType, _BrokenDisposeAsyncDep); + expect(events[16].dep?.depHashCode, events[5].dep?.depHashCode); + expect(events[16].scope.scopeHashCode, events[0].scope.scopeHashCode); + + expect(events[17].name, 'onValueCleared'); + expect(events[17].scope.type, _BrokenAsyncDepScopeContainer); + expect(events[17].dep?.valueType, _TestAsyncDep); + expect(events[17].dep?.depHashCode, events[1].dep?.depHashCode); + expect(events[17].scope.scopeHashCode, events[0].scope.scopeHashCode); + + expect(events[18].name, 'onScopeDisposed'); + expect(events[18].scope.type, _BrokenAsyncDepScopeContainer); + expect(events[18].scope.scopeHashCode, events[0].scope.scopeHashCode); + + expect(events.length, 19); + }); + + test('create failure calls in correct order with correct params', () async { + final listener = TestObserver(); + final holder = _BrokenAsyncDepScopeHolder(listener, checkDispose: true); + + await holder.create(); + + try { + holder.scope?.brokenCreateDep.get; + // should not produce any listener event + holder.scope?.testAsyncDep.get; + } catch (e) { + expect(e, isA()); + } + try { + await holder.drop(); + } catch (_) { + fail('Drop must complete anyway'); + } + + final events = listener._events; + expect(events[0].name, 'onScopeStartInitialize'); + expect(events[0].scope.type, _BrokenAsyncDepScopeContainer); + + expect(events[1].name, 'onValueStartCreate'); + expect(events[1].scope.type, _BrokenAsyncDepScopeContainer); + expect(events[1].dep?.valueType, _TestAsyncDep); + expect(events[1].scope.scopeHashCode, events[0].scope.scopeHashCode); + + expect(events[2].name, 'onValueCreated'); + expect(events[2].scope.type, _BrokenAsyncDepScopeContainer); + expect(events[2].dep?.valueType, _TestAsyncDep); + expect(events[2].dep?.depHashCode, events[1].dep?.depHashCode); + expect(events[2].scope.scopeHashCode, events[0].scope.scopeHashCode); + + expect(events[3].name, 'onDepStartInitialize'); + expect(events[3].scope.type, _BrokenAsyncDepScopeContainer); + expect(events[3].dep?.valueType, _TestAsyncDep); + expect(events[3].dep?.depHashCode, events[1].dep?.depHashCode); + expect(events[3].scope.scopeHashCode, events[0].scope.scopeHashCode); + + expect(events[4].name, 'onDepInitialized'); + expect(events[4].scope.type, _BrokenAsyncDepScopeContainer); + expect(events[4].dep?.valueType, _TestAsyncDep); + expect(events[4].dep?.depHashCode, events[1].dep?.depHashCode); + expect(events[4].scope.scopeHashCode, events[0].scope.scopeHashCode); + + expect(events[5].name, 'onValueStartCreate'); + expect(events[5].scope.type, _BrokenAsyncDepScopeContainer); + expect(events[5].dep?.valueType, _BrokenDisposeAsyncDep); + expect(events[5].scope.scopeHashCode, events[0].scope.scopeHashCode); + + expect(events[6].name, 'onValueCreated'); + expect(events[6].scope.type, _BrokenAsyncDepScopeContainer); + expect(events[6].dep?.valueType, _BrokenDisposeAsyncDep); + expect(events[6].dep?.depHashCode, events[5].dep?.depHashCode); + expect(events[6].scope.scopeHashCode, events[0].scope.scopeHashCode); + + expect(events[7].name, 'onDepStartInitialize'); + expect(events[7].scope.type, _BrokenAsyncDepScopeContainer); + expect(events[7].dep?.valueType, _BrokenDisposeAsyncDep); + expect(events[7].dep?.depHashCode, events[5].dep?.depHashCode); + expect(events[7].scope.scopeHashCode, events[0].scope.scopeHashCode); + + expect(events[8].name, 'onDepInitialized'); + expect(events[8].scope.type, _BrokenAsyncDepScopeContainer); + expect(events[8].dep?.valueType, _BrokenDisposeAsyncDep); + expect(events[8].dep?.depHashCode, events[5].dep?.depHashCode); + expect(events[8].scope.scopeHashCode, events[0].scope.scopeHashCode); + + expect(events[9].name, 'onScopeInitialized'); + expect(events[9].scope.type, _BrokenAsyncDepScopeContainer); + expect(events[9].scope.scopeHashCode, events[0].scope.scopeHashCode); + + expect(events[10].name, 'onValueStartCreate'); + expect(events[10].scope.type, _BrokenAsyncDepScopeContainer); + expect(events[10].dep?.valueType, NeverCreatedValue); + expect(events[10].scope.scopeHashCode, events[0].scope.scopeHashCode); + + expect(events[11].name, 'onValueCreateFailed'); + expect(events[11].scope.type, _BrokenAsyncDepScopeContainer); + expect(events[11].dep?.valueType, NeverCreatedValue); + expect(events[11].scope.scopeHashCode, events[0].scope.scopeHashCode); + + expect(events[12].name, 'onScopeStartDispose'); + expect(events[12].scope.type, _BrokenAsyncDepScopeContainer); + expect(events[12].scope.scopeHashCode, events[0].scope.scopeHashCode); + + expect(events[13].name, 'onDepStartDispose'); + expect(events[13].scope.type, _BrokenAsyncDepScopeContainer); + expect(events[13].dep?.valueType, _BrokenDisposeAsyncDep); + expect(events[13].dep?.depHashCode, events[5].dep?.depHashCode); + expect(events[13].scope.scopeHashCode, events[0].scope.scopeHashCode); + + expect(events[14].name, 'onDepDisposeFailed'); + expect(events[14].scope.type, _BrokenAsyncDepScopeContainer); + expect(events[14].dep?.valueType, _BrokenDisposeAsyncDep); + expect(events[14].dep?.depHashCode, events[5].dep?.depHashCode); + expect(events[14].scope.scopeHashCode, events[0].scope.scopeHashCode); + expect(events[14].exception, isA()); + + expect(events[15].name, 'onScopeDisposeDepFailed'); + expect(events[15].scope.type, _BrokenAsyncDepScopeContainer); + expect(events[15].dep?.valueType, _BrokenDisposeAsyncDep); + expect(events[15].dep?.depHashCode, events[5].dep?.depHashCode); + expect(events[15].scope.scopeHashCode, events[0].scope.scopeHashCode); + expect(events[15].exception, isA()); + + expect(events[16].name, 'onDepStartDispose'); + expect(events[16].scope.type, _BrokenAsyncDepScopeContainer); + expect(events[16].dep?.valueType, _TestAsyncDep); + expect(events[16].dep?.depHashCode, events[1].dep?.depHashCode); + expect(events[16].scope.scopeHashCode, events[0].scope.scopeHashCode); + + expect(events[17].name, 'onDepDisposed'); + expect(events[17].scope.type, _BrokenAsyncDepScopeContainer); + expect(events[17].dep?.valueType, _TestAsyncDep); + expect(events[17].dep?.depHashCode, events[1].dep?.depHashCode); + expect(events[17].scope.scopeHashCode, events[0].scope.scopeHashCode); + + expect(events[18].name, 'onValueCleared'); + expect(events[18].scope.type, _BrokenAsyncDepScopeContainer); + expect(events[18].dep?.valueType, NeverCreatedValue); + expect(events[18].dep?.depHashCode, events[10].dep?.depHashCode); + expect(events[18].scope.scopeHashCode, events[0].scope.scopeHashCode); + + expect(events[19].name, 'onValueCleared'); + expect(events[19].scope.type, _BrokenAsyncDepScopeContainer); + expect(events[19].dep?.valueType, _BrokenDisposeAsyncDep); + expect(events[19].dep?.depHashCode, events[5].dep?.depHashCode); + expect(events[19].scope.scopeHashCode, events[0].scope.scopeHashCode); + + expect(events[20].name, 'onValueCleared'); + expect(events[20].scope.type, _BrokenAsyncDepScopeContainer); + expect(events[20].dep?.valueType, _TestAsyncDep); + expect(events[20].dep?.depHashCode, events[1].dep?.depHashCode); + expect(events[20].scope.scopeHashCode, events[0].scope.scopeHashCode); + + expect(events[21].name, 'onScopeDisposed'); + expect(events[21].scope.type, _BrokenAsyncDepScopeContainer); + expect(events[21].scope.scopeHashCode, events[0].scope.scopeHashCode); + + expect(events.length, 22); + }); +} + +class ObserverEvent { + final String name; + final ScopeId scope; + final DepId? dep; + final ValueMeta? value; + final Object? exception; + final StackTrace? stackTrace; + + ObserverEvent({ + required this.name, + required this.scope, + this.dep, + this.value, + this.exception, + this.stackTrace, + }); + + @override + String toString() => + 'ObserverEvent{name: $name, scope: $scope, dep: $dep, value: $value, ' + 'exception: $exception, stackTrace: $stackTrace}'; +} + +class TestObserver implements ScopeObserver, DepObserver, AsyncDepObserver { + final _events = []; + + TestObserver(); + + void _log( + String name, + ScopeId scope, { + DepId? dep, + ValueMeta? value, + Object? exception, + StackTrace? stackTrace, + }) { + print('$name: $scope, $dep, $value'); + if (exception != null) { + print('$exception\n$stackTrace'); + } + _events.add( + ObserverEvent( + name: name, + scope: scope, + dep: dep, + value: value, + exception: exception, + stackTrace: stackTrace, + ), + ); + } + + @override + void onScopeStartInitialize(ScopeId scope) => + _log('onScopeStartInitialize', scope); + + @override + void onScopeInitialized(ScopeId scope) => _log('onScopeInitialized', scope); + + @override + void onScopeInitializeFailed( + ScopeId scope, + Object exception, + StackTrace stackTrace, + ) => + _log( + 'onScopeInitializeFailed', + scope, + exception: exception, + stackTrace: stackTrace, + ); + + @override + void onScopeStartDispose(ScopeId scope) => _log('onScopeStartDispose', scope); + + @override + void onScopeDisposed(ScopeId scope) => _log('onScopeDisposed', scope); + + @override + void onScopeDisposeDepFailed( + ScopeId scope, + DepId dep, + Object exception, + StackTrace stackTrace, + ) => + _log( + 'onScopeDisposeDepFailed', + scope, + dep: dep, + exception: exception, + stackTrace: stackTrace, + ); + + @override + void onValueCreated(ScopeId scope, DepId dep, ValueMeta? valueMeta) => + _log('onValueCreated', scope, dep: dep, value: valueMeta); + + @override + void onValueStartCreate(ScopeId scope, DepId dep) => + _log('onValueStartCreate', scope, dep: dep); + + @override + void onValueCreateFailed( + ScopeId scope, + DepId dep, + Object exception, + StackTrace stackTrace, + ) => + _log( + 'onValueCreateFailed', + scope, + dep: dep, + exception: exception, + stackTrace: stackTrace, + ); + + @override + void onValueCleared(ScopeId scope, DepId dep, ValueMeta? valueMeta) => + _log('onValueCleared', scope, dep: dep, value: valueMeta); + + @override + void onDepStartInitialize(ScopeId scope, DepId dep) => + _log('onDepStartInitialize', scope, dep: dep); + + @override + void onDepInitialized(ScopeId scope, DepId dep) => + _log('onDepInitialized', scope, dep: dep); + + @override + void onDepStartDispose(ScopeId scope, DepId dep) => + _log('onDepStartDispose', scope, dep: dep); + + @override + void onDepDisposed(ScopeId scope, DepId dep) => + _log('onDepDisposed', scope, dep: dep); + + @override + void onDepInitializeFailed( + ScopeId scope, + DepId dep, + Object exception, + StackTrace stackTrace, + ) => + _log( + 'onDepInitializeFailed', + scope, + dep: dep, + exception: exception, + stackTrace: stackTrace, + ); + + @override + void onDepDisposeFailed( + ScopeId scope, + DepId dep, + Object exception, + StackTrace stackTrace, + ) => + _log( + 'onDepDisposeFailed', + scope, + dep: dep, + exception: exception, + stackTrace: stackTrace, + ); +} + +class _TestScopeContainer extends ScopeContainer { + @override + List> get initializeQueue => [ + {myAsyncDep} + ]; + + late final myAsyncDep = asyncDep(() => _TestAsyncDep()); + + late final syncDep = dep(() => _SyncDep()); +} + +class _TestScopeHolder extends ScopeHolder<_TestScopeContainer> { + _TestScopeHolder(TestObserver listener) + : super( + scopeObservers: [listener], + depObservers: [listener], + asyncDepObservers: [listener], + ); + + @override + _TestScopeContainer createContainer() => _TestScopeContainer(); +} + +class _SyncDep {} + +class _TestAsyncDep implements AsyncLifecycle { + @override + Future init() async {} + + @override + Future dispose() async {} +} + +class _BrokenAsyncDepScopeContainer extends ScopeContainer { + final bool checkDispose; + + _BrokenAsyncDepScopeContainer({required this.checkDispose}); + + @override + List> get initializeQueue => [ + {testAsyncDep}, + if (!checkDispose) {brokenInitDep}, + {brokenDisposeDep}, + ]; + + late final testAsyncDep = asyncDep(() => _TestAsyncDep()); + + late final brokenInitDep = asyncDep(() => _BrokenInitAsyncDep()); + + late final brokenDisposeDep = asyncDep(() => _BrokenDisposeAsyncDep()); + + late final brokenCreateDep = dep(() => NeverCreatedValue()); +} + +class _BrokenAsyncDepScopeHolder + extends ScopeHolder<_BrokenAsyncDepScopeContainer> { + final bool checkDispose; + + _BrokenAsyncDepScopeHolder(TestObserver listener, {this.checkDispose = false}) + : super( + scopeObservers: [listener], + depObservers: [listener], + asyncDepObservers: [listener], + ); + + @override + _BrokenAsyncDepScopeContainer createContainer() => + _BrokenAsyncDepScopeContainer(checkDispose: checkDispose); +} + +class _BrokenInitAsyncDep implements AsyncLifecycle { + @override + Future init() async { + throw BrokenInitException(); + } + + @override + Future dispose() async {} +} + +class _BrokenDisposeAsyncDep implements AsyncLifecycle { + @override + Future init() async {} + + @override + Future dispose() async { + throw BrokenDisposeException(); + } +} + +class NeverCreatedValue { + NeverCreatedValue() { + throw BrokenCreateException(); + } +} + +class BrokenCreateException implements Exception {} + +class BrokenInitException implements Exception {} + +class BrokenDisposeException implements Exception {} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/test/parent_scope_test.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/test/parent_scope_test.dart new file mode 100644 index 0000000..329fb15 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/test/parent_scope_test.dart @@ -0,0 +1,265 @@ +import 'package:test/test.dart'; +import 'package:yx_scope/yx_scope.dart'; + +import 'utils/test_logger.dart'; + +void main() { + setUp(() { + ScopeObservatory.logger = const TestLogger(); + }); + + test('child scope is not created with the parent', () async { + final parentHolder = _ParentScopeHolder(); + await parentHolder.create(); + final childHolder = parentHolder.scope?.childScopeDep.get; + + expect(parentHolder.scope, isNotNull); + expect(childHolder, isNotNull); + expect(childHolder?.scope, isNull); + }); + + test('child scope disposed when parent scope dropped', () async { + final parentHolder = _ParentScopeHolder(); + await parentHolder.create(); + final childHolder = parentHolder.scope?.childScopeDep.get; + + expect(parentHolder.scope, isNotNull); + expect(childHolder, isNotNull); + expect(childHolder?.scope, isNull); + + final testData = _TestData(); + await childHolder?.create(testData); + + expect(parentHolder.scope, isNotNull); + expect(childHolder, isNotNull); + expect(childHolder?.scope, isNotNull); + + final actualData = childHolder?.scope?.data; + final actualChildAsyncDep = childHolder?.scope?.childAsyncDep.get; + expect(actualChildAsyncDep?._initialized, isTrue); + + expect(actualData, testData); + + await parentHolder.drop(); + + expect(parentHolder.scope, isNull); + expect(childHolder?.scope, isNull); + expect(actualChildAsyncDep?._initialized, isFalse); + }); + + test('parent dep instances are the same as provided in child', () async { + final parentHolder = _ParentScopeHolder(); + await parentHolder.create(); + final childHolder = parentHolder.scope?.childScopeDep.get; + + expect(parentHolder.scope, isNotNull); + expect(childHolder, isNotNull); + expect(childHolder?.scope, isNull); + + final testData = _TestData(); + await childHolder?.create(testData); + + expect(parentHolder.scope, isNotNull); + expect(childHolder, isNotNull); + expect(childHolder?.scope, isNotNull); + + // data is not created twice + expect( + parentHolder.scope?.childScopeDep.get.scope?.data, + parentHolder.scope?.childScopeDep.get.scope?.data, + ); + + expect( + parentHolder.scope?.parentDep.get, + parentHolder.scope?.childScopeDep.get.scope?.childDep.get.parentDep, + ); + expect( + parentHolder.scope?.parentAsyncDep.get, + parentHolder.scope?.childScopeDep.get.scope?.childDep.get.parentAsyncDep, + ); + expect( + testData, + parentHolder.scope?.childScopeDep.get.scope?.childDep.get.data, + ); + expect( + parentHolder.scope?.parentDep.get, + parentHolder.scope?.childScopeDep.get.scope?.childAsyncDep.get.parentDep, + ); + expect( + parentHolder.scope?.parentAsyncDep.get, + parentHolder + .scope?.childScopeDep.get.scope?.childAsyncDep.get.parentAsyncDep, + ); + + await childHolder?.drop(); + + expect(parentHolder.scope, isNotNull); + expect(childHolder, isNotNull); + expect(childHolder?.scope, isNull); + }); + + test( + 'Two different scope trees can contain deps with the same type and its different instances', + () async { + final parentHolder1 = _ParentScopeHolder(); + final parentHolder2 = _ParentScopeHolder(); + + expect( + parentHolder1.scope?.parentDep.get, + parentHolder2.scope?.parentDep.get, + ); + expect( + parentHolder1.scope?.parentAsyncDep.get, + parentHolder2.scope?.parentAsyncDep.get, + ); + expect( + parentHolder1.scope?.childScopeDep.get.scope?.childDep.get, + parentHolder2.scope?.childScopeDep.get.scope?.childDep.get, + ); + expect( + parentHolder1.scope?.childScopeDep.get.scope?.childAsyncDep.get, + parentHolder2.scope?.childScopeDep.get.scope?.childAsyncDep.get, + ); + }); +} + +class _ParentScopeContainer extends ScopeContainer { + @override + List> get initializeQueue => [ + {parentAsyncDep} + ]; + + late final childScopeDep = dep(() => _ChildScopeHolder(this)); + + late final childWithDuplicationScopeDep = + dep(() => _ChildWithDuplicationScopeHolder(this)); + + late final parentDep = dep(() => _ParentDep()); + + late final parentAsyncDep = asyncDep(() => _ParentAsyncDep()); +} + +class _ParentScopeHolder extends ScopeHolder<_ParentScopeContainer> { + @override + _ParentScopeContainer createContainer() => _ParentScopeContainer(); +} + +class _ChildScopeContainer + extends ChildDataScopeContainer<_ParentScopeContainer, _TestData> { + _ChildScopeContainer({ + required _ParentScopeContainer parent, + required _TestData data, + }) : super(parent: parent, data: data); + + @override + List> get initializeQueue => [ + {childAsyncDep} + ]; + + late final childDep = dep( + () => _ChildDep( + data, + parent.parentDep.get, + parent.parentAsyncDep.get, + ), + ); + + late final childAsyncDep = asyncDep( + () => _ChildAsyncDep( + parent.parentDep.get, + parent.parentAsyncDep.get, + childDep.get, + ), + ); +} + +class _ChildScopeHolder extends ChildDataScopeHolder<_ChildScopeContainer, + _ParentScopeContainer, _TestData> { + _ChildScopeHolder(_ParentScopeContainer parent) : super(parent); + + @override + _ChildScopeContainer createContainer( + _ParentScopeContainer parent, + _TestData data, + ) => + _ChildScopeContainer(data: data, parent: parent); +} + +class _ChildWithDuplicationScopeContainer + extends ChildScopeContainer<_ParentScopeContainer> { + _ChildWithDuplicationScopeContainer({required _ParentScopeContainer parent}) + : super(parent: parent); + + late final duplicatedParentDep = dep(() => _ParentDep()); + + late final duplicatedParentAsyncDep = asyncDep(() => _ParentAsyncDep()); + + late final duplicatedChildDep = dep( + () => _ChildDep( + _TestData(), + parent.parentDep.get, + parent.parentAsyncDep.get, + ), + ); +} + +class _ChildWithDuplicationScopeHolder extends ChildScopeHolder< + _ChildWithDuplicationScopeContainer, _ParentScopeContainer> { + _ChildWithDuplicationScopeHolder(_ParentScopeContainer parent) + : super(parent); + + @override + _ChildWithDuplicationScopeContainer createContainer( + _ParentScopeContainer parent) => + _ChildWithDuplicationScopeContainer(parent: parent); +} + +class _ParentDep {} + +class _ParentAsyncDep implements AsyncLifecycle { + // ignore: unused_field + var _initialized = false; + + @override + Future init() async { + _initialized = true; + } + + @override + Future dispose() async { + _initialized = false; + } +} + +class _ChildDep { + final _TestData data; + final _ParentDep parentDep; + final _ParentAsyncDep parentAsyncDep; + + const _ChildDep(this.data, this.parentDep, this.parentAsyncDep); +} + +class _ChildAsyncDep implements AsyncLifecycle { + final _ParentDep parentDep; + final _ParentAsyncDep parentAsyncDep; + final _ChildDep childDep; + var _initialized = false; + + _ChildAsyncDep( + this.parentDep, + this.parentAsyncDep, + this.childDep, + ); + + @override + Future init() async { + _initialized = true; + } + + @override + Future dispose() async { + _initialized = false; + } +} + +class _TestData {} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/test/scope_state_streamable_test.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/test/scope_state_streamable_test.dart new file mode 100644 index 0000000..826b389 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/test/scope_state_streamable_test.dart @@ -0,0 +1,229 @@ +import 'package:test/test.dart'; +import 'package:yx_scope/src/base_scope_container.dart'; +import 'package:yx_scope/yx_scope.dart'; + +import 'utils/test_logger.dart'; + +void main() { + setUp(() { + ScopeObservatory.logger = const TestLogger(); + }); + + group('stream of scope', () { + test('stream for scope happy path', () async { + final holder = _TestScopeHolder(); + + int counter = 0; + holder.stream.listen((scope) { + if (counter % 2 == 0) { + expect(scope, isNotNull); + } else { + expect(scope, isNull); + } + }); + + await holder.create(); + counter++; + await holder.drop(); + counter++; + await holder.create(); + counter++; + await holder.drop(); + counter++; + }); + + test('after pausing stream of scope it emits buffered events', () async { + final holder = _TestScopeHolder(); + + int counter = 0; + final subscription = holder.stream.listen((scope) { + if (counter % 2 == 0) { + expect(scope, isNotNull); + } else { + expect(scope, isNull); + } + counter++; + }); + + await holder.create(); + await holder.drop(); + + subscription.pause(); + + await holder.create(); + await holder.drop(); + await holder.create(); + + subscription.resume(); + + await holder.drop(); + await holder.create(); + await holder.drop(); + + expect(counter, 7); + }); + + test('reusing the same instance of stream cause an exception', () async { + final holder = _TestScopeHolder(); + final stream = holder.stream; + + stream.listen((_) {}); + try { + stream.listen((_) {}); + fail('Must throw an exception'); + } on StateError catch (e) { + expect(e.message, 'Stream has already been listened to.'); + } + }); + + test('two different get calls return different streams', () async { + final holder = _TestScopeHolder(); + final stream1 = holder.stream; + final stream2 = holder.stream; + expect(stream1, isNot(stream2)); + }); + + test('ask for a stream creates new listener and cancelling removes it', + () async { + final holder = _TestScopeHolder(); + final testHolder = TestableScopeStateHolder(holder); + final stream1 = holder.stream; + expect(testHolder.listeners.isEmpty, isTrue); + final sub1 = stream1.listen((_) {}); + expect(testHolder.listeners.length, 1); + final stream2 = holder.stream; + expect(testHolder.listeners.length, 1); + final sub2 = stream2.listen((_) {}); + expect(testHolder.listeners.length, 2); + await sub1.cancel(); + expect(testHolder.listeners.length, 1); + await sub2.cancel(); + expect(testHolder.listeners.isEmpty, isTrue); + }); + }); + + group('stream of state', () { + test('stream for scope happy path', () async { + final holder = _TestScopeHolder(); + + int counter = 0; + holder.stateStream.listen((state) { + switch (counter % 4) { + case 0: + expect(state, isA>()); + break; + case 1: + expect(state, isA>()); + expect((state as ScopeStateAvailable).scope, + isA<_TestScopeContainer>()); + break; + case 2: + expect(state, isA>()); + break; + case 3: + expect(state, isA>()); + break; + default: + } + counter++; + }); + + await holder.create(); + await holder.drop(); + await holder.create(); + await holder.drop(); + await holder.create(); + await holder.drop(); + }); + + test('after pausing stream of scope it emits buffered events', () async { + final holder = _TestScopeHolder(); + + int counter = 0; + final subscription = holder.stateStream.listen((state) { + print(counter); + print(state); + switch (counter % 4) { + case 0: + expect(state, isA>()); + break; + case 1: + expect(state, isA>()); + expect((state as ScopeStateAvailable).scope, + isA<_TestScopeContainer>()); + break; + case 2: + expect(state, isA>()); + break; + case 3: + expect(state, isA>()); + break; + default: + } + counter++; + }); + + await holder.create(); + await holder.drop(); + + subscription.pause(); + + await holder.create(); + await holder.drop(); + await holder.create(); + + subscription.resume(); + + await holder.drop(); + await holder.create(); + await holder.drop(); + + expect(counter, 8); + }); + + test('reusing the same instance of stream cause an exception', () async { + final holder = _TestScopeHolder(); + final stream = holder.stateStream; + + stream.listen((_) {}); + try { + stream.listen((_) {}); + fail('Must throw an exception'); + } on StateError catch (e) { + expect(e.message, 'Stream has already been listened to.'); + } + }); + + test('two different get calls return different streams', () async { + final holder = _TestScopeHolder(); + final stream1 = holder.stateStream; + final stream2 = holder.stateStream; + expect(stream1, isNot(stream2)); + }); + + test('ask for a stream creates new listener and cancelling removes it', + () async { + final holder = _TestScopeHolder(); + final testHolder = TestableScopeStateHolder(holder); + final stream1 = holder.stateStream; + expect(testHolder.listeners.isEmpty, isTrue); + final sub1 = stream1.listen((_) {}); + expect(testHolder.listeners.length, 1); + final stream2 = holder.stateStream; + expect(testHolder.listeners.length, 1); + final sub2 = stream2.listen((_) {}); + expect(testHolder.listeners.length, 2); + await sub1.cancel(); + expect(testHolder.listeners.length, 1); + await sub2.cancel(); + expect(testHolder.listeners.isEmpty, isTrue); + }); + }); +} + +class _TestScopeContainer extends ScopeContainer {} + +class _TestScopeHolder extends ScopeHolder<_TestScopeContainer> { + @override + _TestScopeContainer createContainer() => _TestScopeContainer(); +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/test/scope_state_test.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/test/scope_state_test.dart new file mode 100644 index 0000000..cf152c0 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/test/scope_state_test.dart @@ -0,0 +1,53 @@ +import 'dart:async'; + +import 'package:test/test.dart'; +import 'package:yx_scope/yx_scope.dart'; + +void main() { + test('scope state is valid according to updates', () async { + final scopeHolder = _TestScopeHolder(); + + final createCompleter = Completer(); + final dropCompleter = Completer(); + + expect(scopeHolder.state, isA>()); + expect(scopeHolder.state.none, isTrue); + scopeHolder.create().then((_) => createCompleter.complete()); + + expect( + scopeHolder.state, isA>()); + expect(scopeHolder.state.initializing, isTrue); + await createCompleter.future; + + expect(scopeHolder.state, isA>()); + expect(scopeHolder.state.available, isTrue); + + scopeHolder.drop().then((_) => dropCompleter.complete()); + + expect(scopeHolder.state, isA>()); + expect(scopeHolder.state.disposing, isTrue); + + await dropCompleter.future; + + expect(scopeHolder.state, isA>()); + expect(scopeHolder.state.none, isTrue); + }); +} + +class _TestScopeHolder extends ScopeHolder<_TestScopeContainer> { + @override + _TestScopeContainer createContainer() => _TestScopeContainer(); +} + +class _TestScopeContainer extends ScopeContainer { + @override + List> get initializeQueue => [ + {_asyncDep} + ]; + + late final _asyncDep = rawAsyncDep( + () => Future.delayed(Duration.zero), + init: (dep) async => await dep, + dispose: (dep) async {}, + ); +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/test/scope_test.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/test/scope_test.dart new file mode 100644 index 0000000..468c55d --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/test/scope_test.dart @@ -0,0 +1,354 @@ +import 'dart:async'; + +import 'package:test/test.dart'; +import 'package:yx_scope/yx_scope.dart'; + +import 'utils/test_logger.dart'; +import 'utils/utils.dart'; + +void main() { + setUp(() { + ScopeObservatory.logger = const TestLogger(); + }); + + test('create and dispose scope works', () async { + final holder = _TestScopeHolder(); + expect(holder.scope, isNull); + + await holder.create(); + expect(holder.scope, isNotNull); + + final scope = holder.scope; + expect(ScopeContainerTestUtils.getDepCount(scope!), 1); + await holder.drop(); + expect(ScopeContainerTestUtils.getDepCount(scope), 0); + expect(holder.scope, isNull); + }); + + test('second initialization call fails with an exception', () async { + final holder = _TestScopeHolder(); + expect(holder.scope, isNull); + + await holder.create(); + await expectThrown(() async => holder.create()); + }); + + test('unawaited second initialization call fails with an exception', + () async { + final holder = _TestScopeHolder(); + expect(holder.scope, isNull); + + // ignore: unawaited_futures + holder.create(); + await expectThrown(() async => holder.create()); + }); + + test( + 'sync drop call during initialization waits until fully initialized and then drops', + () async { + final depOverride = _TestAsyncDep(); + final holder = _TestScopeHolder(asyncDepOverride: depOverride); + expect(holder.scope, isNull); + + // ignore: unawaited_futures + holder.create(); + await holder.drop(); + expect(depOverride._initialized, isFalse); + expect(holder.scope, isNull); + }); + + test( + 'sync create call during dispose waits until fully disposed and then init', + () async { + final depOverride = _TestAsyncDep(); + final holder = _TestScopeHolder(asyncDepOverride: depOverride); + expect(holder.scope, isNull); + + await holder.create(); + // ignore: unawaited_futures + holder.drop(); + await holder.create(); + expect(depOverride._initialized, isTrue); + expect(holder.scope, isNotNull); + }); + + test( + 'second unawaited call for create after unawaited drop throws an exception', + () async { + final depOverride = _TestAsyncDep(); + final holder = _TestScopeHolder(asyncDepOverride: depOverride); + expect(holder.scope, isNull); + + await holder.create(); + // ignore: unawaited_futures + holder.drop(); + // ignore: unawaited_futures + holder.create(); + // ignore: unawaited_futures + expectThrown(() async => await holder.create()); + }); + + test( + 'second unawaited call for drop after unawaited create throws an exception', + () async { + final depOverride = _TestAsyncDep(); + final holder = _TestScopeHolder(asyncDepOverride: depOverride); + expect(holder.scope, isNull); + + await holder.create(); + // ignore: unawaited_futures + await holder.drop(); + // ignore: unawaited_futures + holder.create(); + // ignore: unawaited_futures + holder.drop(); + expectThrown(() async => await holder.drop()); + }); + + test('sync create call after drop throw an assertion', () async { + final depOverride = _TestAsyncDep(); + final holder = _TestScopeHolder(asyncDepOverride: depOverride); + expect(holder.scope, isNull); + + await holder.create(); + // ignore: unawaited_futures + holder.drop(); + await holder.create(); + expect(holder.scope, isNotNull); + expect(holder.scope?.myAsyncDep.get._initialized, isTrue); + }); + + test('sync second drop call fails with an assertion', () async { + final depOverride = _TestAsyncDep(); + final holder = _TestScopeHolder(asyncDepOverride: depOverride); + expect(holder.scope, isNull); + + await holder.create(); + // ignore: unawaited_futures + holder.drop(); + await expectAssertion(() async => holder.drop()); + }); + + test('handling exception during init and dispose of async deps', () async { + final testAsyncDep = _TestAsyncDep(); + final brokenInitAsyncDep = _BrokenInitAsyncDep(); + final brokenDisposeAsyncDep = _BrokenDisposeAsyncDep(); + final holder = _BrokenAsyncDepScopeHolder( + asyncDepOverride: testAsyncDep, + brokenInitAsyncDepOverride: brokenInitAsyncDep, + brokenDisposeAsyncDepOverride: brokenDisposeAsyncDep, + ); + + try { + await holder.create(); + fail('Must be an exception'); + } on BrokenInitException catch (_) { + expect(testAsyncDep._initialized, isFalse); + expect(brokenInitAsyncDep._disposed, isFalse); + expect(brokenDisposeAsyncDep._initialized, isTrue); + } + + expect(holder.scope, isNull); + }); + + test( + 'ScopeHolder creates different instances ' + 'with different deps each new time', () async { + final holder = _TestScopeHolder(); + expect(holder.scope, isNull); + + _TestScopeContainer? lastScope; + var scopeCounter = 0; + var noScopeCounter = 0; + final completer = Completer(); + final removeListener = holder.listen((scope) { + if (scope != null) { + expect(lastScope, isNot(scope)); + lastScope = scope; + scopeCounter++; + } else { + noScopeCounter++; + } + if (scopeCounter + noScopeCounter == 6) { + completer.complete(); + } + }); + + await holder.create(); + + final scope1 = holder.scope; + final dep1 = holder.scope?.myAsyncDep.get; + expect(scope1, isNotNull); + expect(dep1, isNotNull); + + await holder.drop(); + + expect(holder.scope, isNull); + + await holder.create(); + + final scope2 = holder.scope; + final dep2 = holder.scope?.myAsyncDep.get; + expect(scope2, isNotNull); + expect(dep2, isNotNull); + + expect(scope1, isNot(scope2)); + expect(dep1, isNot(dep2)); + + await holder.drop(); + + await holder.create(); + + await holder.drop(); + + await completer.future; + + expect(scopeCounter, 3); + expect(noScopeCounter, 3); + + removeListener(); + }); + + test('assertion when dep is called after scope is disposed', () async { + final holder = _TestScopeHolder(); + expect(holder.scope, isNull); + + await holder.create(); + final scope = holder.scope!; + + await holder.drop(); + await expectAssertion(() => scope.myAsyncDep.get); + }); + + test('exception when cached dep is called after scope is disposed', () async { + final holder = _TestScopeHolder(); + expect(holder.scope, isNull); + + await holder.create(); + final scope = holder.scope!; + final dep = scope.syncDep; + + await holder.drop(); + + await expectThrown(() => dep.get); + }); +} + +class _TestScopeContainer extends ScopeContainer { + final _TestAsyncDep? asyncDepOverride; + + _TestScopeContainer({this.asyncDepOverride}); + + @override + List> get initializeQueue => [ + {myAsyncDep} + ]; + + late final myAsyncDep = asyncDep(() => asyncDepOverride ?? _TestAsyncDep()); + + late final syncDep = dep(() => _SyncDep()); +} + +class _TestScopeHolder extends ScopeHolder<_TestScopeContainer> { + final _TestAsyncDep? asyncDepOverride; + + _TestScopeHolder({this.asyncDepOverride}); + + @override + _TestScopeContainer createContainer() => + _TestScopeContainer(asyncDepOverride: asyncDepOverride); +} + +class _SyncDep {} + +class _TestAsyncDep implements AsyncLifecycle { + var _initialized = false; + + @override + Future init() async { + _initialized = true; + } + + @override + Future dispose() async { + _initialized = false; + } +} + +class _BrokenAsyncDepScopeContainer extends ScopeContainer { + final _TestAsyncDep? asyncDepOverride; + final _BrokenInitAsyncDep? brokenInitAsyncDepOverride; + final _BrokenDisposeAsyncDep? brokenDisposeAsyncDepOverride; + + _BrokenAsyncDepScopeContainer({ + this.asyncDepOverride, + this.brokenInitAsyncDepOverride, + this.brokenDisposeAsyncDepOverride, + }); + + @override + List> get initializeQueue => [ + {testDep}, + {brokenDisposeDep}, + {brokenInitDep}, + ]; + + late final testDep = asyncDep(() => asyncDepOverride ?? _TestAsyncDep()); + + late final brokenInitDep = + asyncDep(() => brokenInitAsyncDepOverride ?? _BrokenInitAsyncDep()); + + late final brokenDisposeDep = + asyncDep(() => brokenDisposeAsyncDepOverride ?? _BrokenDisposeAsyncDep()); +} + +class _BrokenAsyncDepScopeHolder + extends ScopeHolder<_BrokenAsyncDepScopeContainer> { + final _TestAsyncDep? asyncDepOverride; + final _BrokenInitAsyncDep? brokenInitAsyncDepOverride; + final _BrokenDisposeAsyncDep? brokenDisposeAsyncDepOverride; + + _BrokenAsyncDepScopeHolder({ + this.asyncDepOverride, + this.brokenInitAsyncDepOverride, + this.brokenDisposeAsyncDepOverride, + }); + + @override + _BrokenAsyncDepScopeContainer createContainer() => + _BrokenAsyncDepScopeContainer( + asyncDepOverride: asyncDepOverride, + brokenInitAsyncDepOverride: brokenInitAsyncDepOverride, + brokenDisposeAsyncDepOverride: brokenDisposeAsyncDepOverride, + ); +} + +class _BrokenInitAsyncDep implements AsyncLifecycle { + var _disposed = false; + + @override + Future init() async { + throw BrokenInitException(); + } + + @override + Future dispose() async { + _disposed = true; + } +} + +class _BrokenDisposeAsyncDep implements AsyncLifecycle { + var _initialized = false; + + @override + Future init() async { + _initialized = true; + } + + @override + Future dispose() async { + throw Exception('broken dispose'); + } +} + +class BrokenInitException implements Exception {} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/test/utils/test_logger.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/test/utils/test_logger.dart new file mode 100644 index 0000000..d0c7ab5 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/test/utils/test_logger.dart @@ -0,0 +1,21 @@ +import 'package:yx_scope/src/monitoring/scope_observatory.dart'; + +class TestLogger extends ScopeLogger { + const TestLogger(); + + @override + void log( + LogType type, + Object message, { + Object? exception, + StackTrace? stackTrace, + }) { + if (type != LogType.error) { + // ignore: avoid_print + print('${type.name}: $message'); + } else { + // ignore: avoid_print + print('ERROR: $message\n$exception\n$stackTrace'); + } + } +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/test/utils/utils.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/test/utils/utils.dart new file mode 100644 index 0000000..5239805 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope/test/utils/utils.dart @@ -0,0 +1,18 @@ +import 'dart:async'; + +import 'package:test/test.dart'; + +Future expectThrown( + FutureOr Function() callback, +) async { + try { + await callback(); + fail('There must be $T thrown'); + // ignore: avoid_catching_errors + } on T catch (_) {} +} + +Future expectAssertion( + FutureOr Function() callback, +) async => + expectThrown(callback); diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/.gitignore b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/.gitignore new file mode 100644 index 0000000..b4de1dc --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/.gitignore @@ -0,0 +1,32 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +.packages +build/ + +.arcignore diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/.metadata b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/.metadata new file mode 100644 index 0000000..d8e3634 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: e2874b6e235bcae425ed3436c113ab4ac50ac707 + channel: unknown + +project_type: package diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/CHANGELOG.md b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/CHANGELOG.md new file mode 100644 index 0000000..de2ea7f --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/CHANGELOG.md @@ -0,0 +1,42 @@ +## 1.1.3 - unpublished + +* Fix: handling of ScopeHolder state changes in ScopeListener subscription + +## 1.1.2 - 2025.07.30 + +* Fix: ScopeBuilder didUpdateWidget throw error if there is no +parent ScopeProvider but scopeHolder passed as argument +* ScopeBuilder test added + +## 1.1.1 - 2025.07.08 + +* Documentation links fixed + +## 1.1.0 - 2025.07.07 + +* Fix: ScopeBuilder and ScopeListener fixes, tests added +* Fix: mounted check added to _subscribe method +* Add test for checking rebuilds only for dependent widgets +* New Flutter version added to CI +* Documentation fixes and updates + +## 1.0.2 - 2024.12.18 + +* Updated repository link +* Library topics added + +## 1.0.1 - 2024.10.23 + +* Minor fixes in links + +## 1.0.0 — 2024.10.18 + +* Ready to be open-source + +## 0.0.2 — 2024.09.23 + +* [ScopeBuilder] API changed: scopeStateHolder -> holder + +## 0.0.1 + +* Add [ScopeProvider], [ScopeConsumer], [ScopeBuilder], [ScopeListener] widgets diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/LICENSE b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/LICENSE new file mode 100644 index 0000000..bd7b143 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2024 YANDEX LLC + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/README.md b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/README.md new file mode 100644 index 0000000..da0cb87 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/README.md @@ -0,0 +1,66 @@ +yx_scope_flutter package is an adapter for yx_scope package for using it with Flutter. + +## Features + +`ScopeProvider` is an `InheritedWidget` that passes your scope down to any widget in the subtree. + +`ScopeBuilder` handles building a widget in response to `scope`. + +`ScopeListener` is a widget that should be used for functionality that needs to occur only in +response to a `scope` change such as navigation, showing a `SnackBar`, showing a `Dialog`, etc... + +`ScopeConsumer` is analogous to a nested `ScopeListener` and `ScopeBuilder` but reduces the amount +of boilerplate needed. + +## Usage + +Lets take a look at how to use `ScopeProvider` to provide `RootScopeHolder` and react to scope +changes with `ScopeBuilder` + +1. Create instance of `RootScopeHolder` and run app + +```dart +void main() { + final rootScopeHolder = RootScopeHolder(); + rootScopeHolder.create(); + runApp(App(scopeHolder: rootScopeHolder)); +} +``` + +2. Use `ScopeProvider` to provide `RootScopeContainer` + +```dart +class App extends StatelessWidget { + final RootScopeHolder scopeHolder; + + const App({required this.scopeHolder, super.key}); + + @override + Widget build(BuildContext context) { + return ScopeProvider( + holder: scopeHolder, + child: MaterialApp( + title: 'YxScopedFlutter Demo', + home: const HomePage(), + ), + ); + } +} +``` + +3. Use `ScopeBuilder` to react to scope changes for build widget + +```dart +// HomePage widget +//... +Widget build() => + ScopeBuilder.withPlaceholder( + builder: (context, rootScope) => SomeWidget(rootScope), + placeholder: const SizedBox.shrink(), + ); +// ... +``` + +## More details + +Read full introduction to the library in [the documentation](https://github.com/yandex/city-services-pub/blob/main/yx_scope/packages/yx_scope_flutter/doc/introduction.md). diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/analysis_options.yaml b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/analysis_options.yaml new file mode 100644 index 0000000..a5744c1 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:flutter_lints/flutter.yaml + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/doc/assets/scope_anatomy.png b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/doc/assets/scope_anatomy.png new file mode 100644 index 0000000..07b5d14 Binary files /dev/null and b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/doc/assets/scope_anatomy.png differ diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/doc/assets/scope_tree.png b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/doc/assets/scope_tree.png new file mode 100644 index 0000000..1c30ee3 Binary files /dev/null and b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/doc/assets/scope_tree.png differ diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/doc/assets/scope_types.png b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/doc/assets/scope_types.png new file mode 100644 index 0000000..974c72f Binary files /dev/null and b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/doc/assets/scope_types.png differ diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/doc/introduction.md b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/doc/introduction.md new file mode 100644 index 0000000..e89e384 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/doc/introduction.md @@ -0,0 +1,543 @@ +## Quick Start + +Let's look at a simple dependency container. First, add yx_scope to your pubspec.yaml: + +```yaml +dependencies: + yx_scope: ^1.0.0 +``` + +Create a file named `app_scope.dart` and add the description of our container and dependencies: + +```dart +class AppScopeContainer extends ScopeContainer { + late final routerDelegateDep = dep(() => AppRouterDelegate()); + + late final appStateObserverDep = dep( + () => + AppStateObserver( + routerDelegateDep.get, + ), + ); +} + +class AppScopeHolder extends ScopeHolder { + @override + AppScopeContainer createContainer() => AppScopeContainer(); +} +``` + +Now, let's create an `AppScopeHolder`, create a container, and access the dependencies: + +```dart +void main() async { + final appScopeHolder = AppScopeHolder(); + await appScopeHolder.create(); + + final appScope = appScopeHolder.scope; + if (appScope != null) { + final AppStateObserver appStateObserver = appScope.appStateObserverDep.get; + } +} +``` + +An important feature of the library is that we work with the DI container without binding it to the +UI. The DI container is primary, and only as an addition to this, the container can be attached to +the UI. + +The DI container is created as a reaction to an event in the application's logic, not as a result of +the appearance of some screen or UI element. UI does not generate scopes; scopes generate UI. + +This is an important principle of the mechanics of yx_scope. + +## Key Entities + +- **Dep (dependency)**: A container for one specific instance of any entity. +- **ScopeContainer**: An isolated, non-overlapping set of dependencies united by a meaningful scope + and sharing a common lifecycle. +- **ScopeHolder**: An instance that stores the current state of the container and is responsible for + its initialization and disposal. + +> A ScopeContainer can be closed by some public interface, hiding implementation details and access +> to Dep. In this case, the interface for it will have the suffix Scope, for example, AccountScope. + +`ScopeHolder` is responsible for creating and removing the scope using the `create` and `drop` +methods. + +Initially, `ScopeHolder` contains a null state. + +After calling the `create` method, a scope appears - a container with dependencies that can be +accessed through the `ScopeHolder`. + +After the `drop` method, all scope dependencies are disposed - the `ScopeHolder` again contains +null. + +Due to null-safety, `ScopeHolder` provides a compile-safe check for the existence of a scope +directly +at the time of writing code, not at runtime. + +![Scope Anatomy](assets/scope_anatomy.png) + +## Scope + +A scope is a container with a set of dependencies, whose lifetime is limited by its life cycle. The +creation and removal of a scope occur based on specific conditions during the application's runtime, +and these **conditions and the life cycle differ from other scopes**. This is extremely +important. The fact that the logic of yx_scope revolves around the concept of a "scope" does not +mean that scopes should be created for everything. A well-structured organization of scope +hierarchies is key to making yx_scope convenient to use. + +You should only create a separate scope when the life cycle of a group of dependencies has clear +conditions for both the start and end of their existence, and these conditions differ from the life +cycles of other scopes. In other words, the start of one scope may not coincide with any other, and +the end may also differ from other scopes. + +> **When a Scope is Not Needed** +> +> If you're unsure what specific condition should trigger the creation of a container with a set of +> dependencies (and what specific condition should cause its termination), then creating a separate +> scope is unnecessary. +> +> If both the start and end of the life cycle of a group of dependencies coincide with those of +> another scope, there is no need to create a separate scope. Such a group of dependencies can be +> placed within an existing scope, and combined using a ScopeModule (which will be discussed later). + +## Scope Tree + +Scopes can relate to each other in two ways: + +1. **Siblings**: Scopes at the same nesting level and in the same visibility area. +2. **Parent-child**: One scope acts as a parent, the other as a child. + +![Scope Tree](assets/scope_tree.png) + +In the diagram, sibling scopes are scopes of the same color. + +- The purple scope is the parent for the blue ones: `Account` and `Register`. +- The blue `AccountScope` is the parent for the green scopes: `Online`, `Order`, `Map`. +- The green `MapScope` is the parent for the yellow `MapNavigationScope`. +- However, the yellow `MapNavigationScope` is not a sibling of a child scope from, for + example, `OrderScope`, even though they are on the same level. + +How to determine the relationship between two scopes? Use the following checks: + +- If two groups of dependencies have different life cycles (LC) that may overlap in time in any + order (or not overlap at all), then you need two sibling scopes. +- If two groups of dependencies have different LCs, but one can only exist strictly within the LC of + the other, then the first scope is a child, and the second is the parent. + +- `AppScope` exists for the entire life of the application. +- `AccountScope` exists within the `AppScope` and appears only when the user logs in, disappearing + when they log out. +- `RegisterScope` exists within the `AppScope` and appears when the user goes to the registration + page. The life cycles of `AccountScope` and `RegisterScope` do not overlap. In the future, a user + might be able to register while already logged in, which would cause the life cycles to overlap, + but they would still not depend on each other. Thus, they are sibling scopes. +- `OnlineScope`, `OrderScope`, and `MapScope` can only exist if the user is logged in and within + the `AccountScope`. The life cycle of each of these three scopes is independent of the + others: `OnlineScope` appears when the user is ready to take orders, `OrderScope` appears when the + user has received an order, and `MapScope` appears when the user navigates to the map screen. All + these scopes must close if the user logs out (i.e., when `AccountScope` closes). +- `MapNavigationScope` — navigation on the map can only occur when the map is open and `MapScope` + exists. However, the map can be open without navigation, meaning the start of the life cycle + of `MapNavigationScope` differs from that of `MapScope`. Therefore, this scope is separated. + +Here's an example of how the scope tree looks in the code, based on `AppScope`: + +```dart +class AppScopeContainer extends ScopeContainer { + + // The child scope of AppScope is simply a nested ScopeHolder of another scope + late final accountScopeHolderDep = dep(() => AccountScopeHolder(this)); + + late final registerScopeHolderDep = dep(() => RegisterScopeHolder(this)); + + late final routerDelegateDep = dep(() => AppRouterDelegate()); + + late final appStateObserverDep = dep( + () => + AppStateObserver( + routerDelegateDep.get, + accountScopeHolderDep.get, + registerScopeHolderDep.get, + ), + ); +} +``` + +## Types of Scopes + +The type of scope depends on two questions: + +1. Does the scope have data without which it cannot exist? +2. Is there a scope without which the new scope cannot exist? + +The combination of answers to these questions will lead us to one of four types: + +- 1 No, 2 No (grey) -> ScopeContainer + ScopeHolder +- 1 Yes, 2 No (blue) -> DataScopeContainer + DataScopeHolder +- 1 No, 2 Yes (green) -> ChildScopeContainer + ChildScopeHolder +- 1 Yes, 2 Yes (green+blue) -> ChildDataScopeContainer + ChildDataScopeHolder + +![Scope Types](assets/scope_types.png) + +## ScopeModule + +If the lifecycle of two groups of dependencies completely coincides, but these dependencies differ +in meaning and you want to separate them from each other, then the correct solution would be to use +`ScopeModule`. + +```dart +class AppScopeContainer extends ScopeContainer { + late final accountScopeHolderDep = dep(() => AccountScopeHolder(this)); + + late final registerScopeHolderDep = dep(() => RegisterScopeHolder(this)); + + // Routing entities in one module + late final routingModule = RoutingAppScopeModule(this); +} + +class RoutingAppScopeModule extends ScopeModule { + RoutingAppScopeModule(super.container); + + late final routerDelegateDep = dep(() => AppRouterDelegate()); + + late final appStateObserverDep = dep( + () => + AppStateObserver( + routerDelegateDep.get, + // We can access deps from the container + container.accountScopeHolderDep.get, + container.registerScopeHolderDep.get, + ), + ); +} +``` + +## Asynchronous Dependencies + +There are dependencies that, in addition to instantiation, require additional initialization, often +asynchronous. Such dependencies are declared in a special way in the container, but most +importantly, they must be specified in the initialization queue for this scope. + +```dart +class AppScopeContainer extends ScopeContainer { + + @override + List> get initializeQueue => + [ + { + appStateObserverDep, + } + ]; + + late final routerDelegateDep = dep(() => AppRouterDelegate()); + + late final appStateObserverDep = rawAsyncDep( + () => + AppStateObserver( + routerDelegateDep.get, + ), + init: (dep) async => dep.init(), + dispose: (dep) async => dep.dispose(), + ); +} +``` + +initializeQueue is an initialization queue consisting of a list of sets of dependencies. All +dependencies within the same set are initialized in parallel (simplified, this can be done via +`Future.wait([])` ([doc](https://api.dart.dev/stable/2.17.3/dart-async/Future/wait.html))). +Dependencies in different sets are initialized sequentially — first, all dependencies in the set +with the lower index in the list are initialized, followed by those in the set with the higher +index. + +The initialization of the entire scope completes once all asynchronous dependencies have been +initialized. That is why the `create()` method is asynchronous. + +There are two ways to make a dependency asynchronous — by using `rawAsyncDep` or by implementing the +`AsyncLifecycle` interface, which contains the init and dispose methods. To declare such a +dependency, you can use the `asyncDep` method. + +The init and dispose methods, as well as their callbacks, are required to be asynchronous. This is +intentional, even for cases where a dependency can be initialized synchronously. Enforcing +asynchronous callbacks ensures that asynchronous calls inside them will not execute without await ( +enforced by the `unawaited_future` lint rule). + +However, if you are certain that a dependency can be initialized after the scope has already +appeared and is ready to work, you can choose not to await its initialization (ignore the linter +using `//ignore: unawaited_future`). + +## Scope Interface + +So far, we've been directly working with the `ScopeContainer` implementation. This is fast and +convenient, but it increases code coupling. In fact, any scope, no matter how deeply nested, can +access any other part of the scope tree through the chain. This creates highly complex and +potentially dangerous interdependencies between scopes and their dependencies. + +A more flexible and scalable approach to working with scope containers is to use interfaces derived +from the base `Scope` interface. This was mentioned earlier in the "Key Entities" section. + +Let's look at an example with `OnlineScope`. Recall that this scope appears when the user selects +that they are ready to receive orders in the application: + +```dart +abstract class OnlineScope implements Scope { + AcceptOrderManager get acceptOrderManager; // manager to accept incoming orders +} + +class OnlineScopeContainer extends ChildScopeContainer + implements OnlineScope { + + OnlineScopeContainer({required super.parent}); + + late final _acceptOrderManagerDep = dep(() => + AcceptOrderManager( + parent.ordersStateHolderDep, + ), + ); + + @override + AcceptOrderManager get acceptOrderManager => _acceptOrderManagerDep.get; +} +``` + +Note: `OnlineScopeContainer` now implements the `OnlineScope` interface, which requires overriding +the getter for `AcceptOrderManager`. We implement it in the container by returning the value from +the `_acceptOrderManagerDep` container. This container can now be made private, meaning no one +outside the scope file will access it directly. + +Let’s also modify the `OnlineScopeHolder`: + +```dart +// Now inherits from BaseChildScopeHolder +class OnlineScopeHolder extends BaseChildScopeHolder { + OnlineScopeHolder(super.parent); + + @override + OnlineScopeContainer createContainer(AccountScopeContainer parent) => + OnlineScopeContainer(parent: parent); +} +``` + +Now `OnlineScopeHolder` inherits from `BaseChildScopeHolder`. This allows us to specify the +interface as the first generic parameter. In turn, this completely hides the container from +consumers: now, when accessing `onlineScopeHolder.scope`, we will work with the `OnlineScope` +interface instead of the container itself. This completely hides the container's origin from users. + +We can apply the same approach with `ScopeHolder`: + +```dart +// This interface is located in the domain layer of the application +abstract class OnlineOrderStateHolder { + Future toggle(); +} + +class OnlineScopeHolder extends BaseChildScopeHolder + implements OnlineOrderStateHolder { + OnlineScopeHolder(super.parent); + + @override + OnlineScopeContainer createContainer(OnlineScopeParent parent) => + OnlineScopeContainer(parent: parent); + + @override + Future toggle() async { + if (scope == null) { + await create(); + } else { + await drop(); + } + } +} +``` + +In this way, in the domain layer of the application, we can work only with +the `OnlineOrderStateHolder` interface, without worrying about its implementation or the fact that +it's a `ScopeHolder`. + +**Tip**: Whenever possible, always try to work with `ScopeHolder` and `ScopeContainer` through +interfaces. This will significantly reduce the coupling between your domain layer and the DI control +entities. + +## Interaction Between Scopes via Interfaces + +We've protected against using raw implementations of scopes from outside, but the scopes themselves +are still interconnected: `OnlineScopeContainer` knows that its parent is `AccountScopeContainer`. +This means it can access any dependency in that container, including holders of other scopes, +potentially creating implicit links between scopes from completely different parts of the tree. + +However, interaction between scopes is still necessary. For example, inside `OrderScope`, we might +want to trigger navigation. Somehow, we need to link scopes across different levels and parents. +This can be done through the parent's `Scope` interface. + +Let’s continue the example with `OnlineScope`: + +```dart +abstract class OnlineScope implements Scope { + AcceptOrderManager get acceptOrderManager; +} + +/// Interface describing what entities the parent must contain +/// to satisfy the expectations of the child scope +abstract class OnlineScopeParent implements Scope { + OrderStateHolder get orderStateHolder; +} + +/// Now ChildScopeContainer is typed not with AccountScopeContainer, +/// but with the interface expected from the parent +class OnlineScopeContainer extends ChildScopeContainer + implements OnlineScope { + + // Dependencies +} + +/// ScopeHolder is also typed with OnlineScopeParent +class OnlineScopeHolder extends BaseChildScopeHolder + implements OnlineOrderStateHolder { + // Implementation +} +``` + +Now `OnlineScope` becomes truly independent. Its consumers use the `OnlineScope` interface, and it +only knows the necessary minimum about its parent to function. + +To ensure that the parent scope meets the required contract, we must implement it: + +```dart +class AccountScopeContainer extends ChildDataScopeContainer + implements AccountScope, OnlineScopeParent { + AccountScopeContainer({ + required super.parent, + required super.data, + }); + + late final _onlineScopeHolderDep = dep(() => OnlineScopeHolder(this)); + + late final _orderScopeHolderDep = dep(() => OrderScopeHolder(this)); + + late final _mapScopeHolderDep = dep(() => MapScopeHolder()); + + @override + OrderStateHolder get orderStateHolder => _orderScopeHolderDep.get; +} +``` + +Here, `AccountScopeContainer` implements both its own `AccountScope` interface and +the `OnlineScopeParent` interface. + +**Tip**: Do not return parent scopes transitively down through interfaces. `OnlineScopeParent` +should not contain a getter for `AccountScopeParent`. This would break isolation by forcing the +scope to implicitly know about the parent's implementation. If you need entities from +the `AccountScopeParent` parent scope, declare them explicitly in the `OnlineScopeParent` interface. + +The benefits of this additional boilerplate is that If all scopes are strictly organized according +to the rule of using interfaces, you can change the hierarchy of your scope tree almost painlessly. +This is crucial in a growing product, as the requirements and life cycles of various app flows may +inevitably change. When moving a child scope from one parent to another, you’ll only need to modify +those parent scopes. The child scope itself will remain unchanged. + +## In the Widget Tree + +The core of `yx_scope` is pure Dart, but the scopes are designed to integrate with Flutter widgets. +For this, you can use the `yx_scope_flutter` package, which provides a set of widget adapters for +your `ScopeHolder` objects. + +Here's a basic example of how to inject a root scope into the widget tree: + +```dart +class App extends StatefulWidget { + const App({super.key}); + + @override + State createState() => _AppState(); +} + +class _AppState extends State { + final _appScopeHolder = AppScopeHolder(); + + @override + void initState() { + super.initState(); + _appScopeHolder.create(); + } + + @override + void dispose() { + _appScopeHolder.drop(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return ScopeProvider( + holder: _appScopeHolder, + // Scope widgets support working with Scope interfaces + child: ScopeBuilder.withPlaceholder( + builder: (context, appScope) { + return MaterialApp.router( + title: 'YxScopedFlutter Demo', + routerDelegate: appScope.routerDelegateDep.get, + ); + }, + // Displays this widget while [appScopeHolder] is loading + placeholder: const Center(child: CircularProgressIndicator()), + ), + ); + } +} +``` + +Notice that `AppScopeHolder` is a field inside the `State` of your app. This is one of the most +significant advantages of `yx_scope`: the entire scope tree (i.e., your whole DI) consists of +non-static `ScopeHolder` instances that are localized to their place in the tree. The +root `ScopeHolder` instance only exists where you instantiate it, and any child `ScopeHolder` +instance only exists if its direct parent scope exists. In some sense, you could call this a +*hyper-local* scoping system. + +## Static Analysis + +Another essential part of `yx_scope` is a set of static analysis rules to guide best practices when +working with the framework. To use it, add the following to your `pubspec.yaml` +under `dev_dependencies`: + +```yaml +dev_dependencies: + yx_scope_linter: ^1.0.0 + custom_lint: ^0.5.3 +``` + +Then, run `pub get`. After that, add this to your `analysis_options.yaml`: + +```yaml +analyzer: + plugins: + - custom_lint +``` + +With this setup, the linter will remind you to add the `Dep` suffix, declare fields as `final`, +highlight uninitialized asynchronous dependencies, detect cyclic dependencies without any code +generation, and even show the full list of entities involved in the cycle. + +This isn't a comprehensive set of rules yet. You can find the current list in the `yx_scope_linter` +README.md. Manual rules not yet automated can be found in `docs/manual_linter.md`, and we’ll +gradually automate them over time. + +--- + +We've now covered the fundamental principles of how the `yx_scope` library works, from global +container basics to scope separation via interfaces and how to link scopes together. + +You can find a more detailed [app example](../example) using scopes in the `yx_scope_flutter` +repository. It also includes an example of creating dynamic scopes of the same type, such +as `OrderScope` for handling multiple orders. + +If you're interested in the issues and motivations behind creating `yx_scope`, check out the [Dev +Day/Night](https://www.youtube.com/watch?v=3hcDcdfk0pQ&list=PLQC2_0cDcSKD7Bfrg0U36Q2KDiy9dLdmb) (ru) +conference talk. Note that the talk covers an earlier version of the library, where it was +called `yx_scoped` and `ScopeContainer` was called `ScopeNode`—don’t let that confuse you. diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/.gitignore b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/.gitignore new file mode 100644 index 0000000..8d0ad39 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/.gitignore @@ -0,0 +1,47 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ +/pubspec.lock + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release + +.arcignore diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/.metadata b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/.metadata new file mode 100644 index 0000000..b3e0d91 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/.metadata @@ -0,0 +1,30 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled. + +version: + revision: e2874b6e235bcae425ed3436c113ab4ac50ac707 + channel: unknown + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: e2874b6e235bcae425ed3436c113ab4ac50ac707 + base_revision: e2874b6e235bcae425ed3436c113ab4ac50ac707 + - platform: android + create_revision: e2874b6e235bcae425ed3436c113ab4ac50ac707 + base_revision: e2874b6e235bcae425ed3436c113ab4ac50ac707 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/README.md b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/README.md new file mode 100644 index 0000000..44a9bf3 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/README.md @@ -0,0 +1,16 @@ +# yx_scope_flutter_example + +A new Flutter project. + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) + +For help getting started with Flutter development, view the +[online documentation](https://docs.flutter.dev/), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/analysis_options.yaml b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/analysis_options.yaml new file mode 100644 index 0000000..c213e8f --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/analysis_options.yaml @@ -0,0 +1,12 @@ +include: package:flutter_lints/flutter.yaml + +linter: + rules: + - implementation_imports # https://dart-lang.github.io/linter/lints/implementation_imports.html + - prefer_relative_imports # https://dart-lang.github.io/linter/lints/prefer_relative_imports.html + - directives_ordering # https://dart-lang.github.io/linter/lints/directives_ordering.html + - unawaited_futures # https://dart-lang.github.io/linter/lints/unawaited_futures.html + +analyzer: + plugins: + - custom_lint diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/data/map/map.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/data/map/map.dart new file mode 100644 index 0000000..f2279d7 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/data/map/map.dart @@ -0,0 +1,64 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; + +class MapController extends ChangeNotifier { + static const totalPositions = 100; + static const itemHeight = 56.0; + + final ScrollController _scrollController; + + final Map items = Map.fromEntries( + List.generate( + 100, + (i) => MapItem(name: 'Street $i', position: i, selected: false), + ).map( + (item) => MapEntry(item.position, item), + ), + ); + + MapController(this._scrollController); + + void selectPosition(int position, {required bool selected}) { + assert( + position >= 0 && position < totalPositions, + 'Your position is out of our simulated map', + ); + items[position] = items[position]!.copyWith(selected: selected); + notifyListeners(); + } + + void clear() { + for (final position in items.keys) { + items[position] = items[position]!.copyWith(selected: false); + } + notifyListeners(); + } + + void focus(int position) { + SchedulerBinding.instance.addPostFrameCallback((_) => + _scrollController.animateTo((position - 2) * itemHeight, + duration: const Duration(milliseconds: 200), curve: Curves.ease)); + } +} + +class MapItem { + final String name; + final int position; + final bool selected; + + const MapItem({ + required this.name, + required this.position, + required this.selected, + }); + + MapItem copyWith({ + bool? selected, + }) { + return MapItem( + selected: selected ?? this.selected, + name: name, + position: position, + ); + } +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/data/map/map_widget.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/data/map/map_widget.dart new file mode 100644 index 0000000..4a4a138 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/data/map/map_widget.dart @@ -0,0 +1,88 @@ +import 'package:flutter/material.dart'; + +import 'map.dart'; + +class MapWidget extends StatefulWidget { + final void Function(MapController map) onMapCreated; + + const MapWidget({ + super.key, + required this.onMapCreated, + }); + + @override + State createState() => _MapWidgetState(); +} + +class _MapWidgetState extends State { + late final Future _future; + late final ScrollController _scrollController; + + @override + void initState() { + super.initState(); + + _scrollController = ScrollController(); + + // Simulate [MapWidget] loading process + _future = Future.delayed( + const Duration(seconds: 1), () => MapController(_scrollController)) + .then((controller) { + if (mounted) { + widget.onMapCreated(controller); + } + return controller; + }); + + _future.then((controller) => controller.addListener(_onControllerUpdate)); + } + + @override + void dispose() { + _future + .then((controller) => controller.removeListener(_onControllerUpdate)); + _scrollController.dispose(); + super.dispose(); + } + + void _onControllerUpdate() => setState(() {}); + + @override + Widget build(BuildContext context) => FutureBuilder( + future: _future, + builder: (context, snap) { + if (!snap.hasData) { + return const Center(child: CircularProgressIndicator()); + } + final controller = snap.requireData; + final list = controller.items.values.toList(growable: false); + return ListView.builder( + controller: _scrollController, + itemCount: controller.items.length, + itemBuilder: (context, index) { + final item = list[index]; + return Theme( + data: Theme.of(context).copyWith( + listTileTheme: Theme.of(context).listTileTheme.copyWith( + selectedTileColor: + Theme.of(context).secondaryHeaderColor), + ), + child: SizedBox( + height: MapController.itemHeight, + child: ListTile( + selected: item.selected, + title: Text(item.name), + onTap: () { + controller.selectPosition( + item.position, + selected: !item.selected, + ); + }, + ), + ), + ); + }, + ); + }, + ); +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/data/orders/incoming_orders_provider.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/data/orders/incoming_orders_provider.dart new file mode 100644 index 0000000..d60f8e3 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/data/orders/incoming_orders_provider.dart @@ -0,0 +1,57 @@ +import 'dart:async'; +import 'dart:math'; + +import 'package:flutter/foundation.dart'; + +import 'models/incoming_order_data.dart'; + +class IncomingOrdersProvider { + IncomingOrderSession get incomingOrderSession => + IncomingOrderSession().._init(); +} + +class IncomingOrderSession { + final _controller = StreamController.broadcast(); + late final StreamSubscription _subscription; + + Stream get incomingOrdersStream => _controller.stream; + + bool get isPaused => _subscription.isPaused; + + void _init() { + _subscription = Stream.periodic( + const Duration(seconds: 5), + (_) => IncomingOrder( + fromAddress: _getRandomAddress(), + toAddress: _getRandomAddress(), + ), + ).listen((order) { + _log('income order ${order.uid}'); + _controller.add(order); + }); + } + + Future dispose() async => await _subscription.cancel(); + + void pause() { + _log('pause to receive orders'); + _subscription.pause(); + } + + void resume() { + _log('resume to receive orders'); + _subscription.resume(); + } + + void _log(String message) { + if (kDebugMode) { + print(message); + } + } +} + +Address _getRandomAddress() { + final position = Random().nextInt(100); + final street = 'Street $position'; + return Address(name: street, position: position); +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/data/orders/models/incoming_order_data.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/data/orders/models/incoming_order_data.dart new file mode 100644 index 0000000..864ae23 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/data/orders/models/incoming_order_data.dart @@ -0,0 +1,26 @@ +import 'dart:math'; + +class IncomingOrder { + static final _random = Random(); + + final DateTime createdTime; + final String uid; + final Address fromAddress; + final Address toAddress; + + IncomingOrder({ + required this.fromAddress, + required this.toAddress, + }) : createdTime = DateTime.now(), + uid = _random.nextInt(100000).toString(); +} + +class Address { + final String name; + final int position; + + const Address({ + required this.name, + required this.position, + }); +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/di/account/account_scope.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/di/account/account_scope.dart new file mode 100644 index 0000000..95e26e7 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/di/account/account_scope.dart @@ -0,0 +1,113 @@ +import 'package:yx_scope/yx_scope.dart'; + +import '../../domain/auth/account_holder.dart'; +import '../../domain/auth/models/account.dart'; +import '../../domain/map/map_factory.dart'; +import '../../domain/order/models/order.dart'; +import '../../domain/order/orders_state_holder.dart'; +import '../../domain/order_navigation/order_navigation_delegate.dart'; +import '../app/app_scope.dart'; +import '../map/map_scope.dart'; +import '../online_order/online_scope.dart'; +import '../order/order_scope.dart'; +import '../utils/listeners.dart'; + +abstract class AccountScope implements Scope { + Account get account; + + OnlineScopeHolder get onlineScopeHolder; + + OrderScopesHolder get orderScopesHolder; + + MapScopeHolder get mapScopeHolder; + + MapInitializer get mapInitializer; +} + +class AccountScopeContainer + extends ChildDataScopeContainer + implements AccountScope, OnlineScopeParent, OrderScopeParent { + AccountScopeContainer({ + required super.parent, + required super.data, + }); + + late final _onlineScopeHolderDep = dep(() => OnlineScopeHolder(this)); + + late final _orderScopesHolderDep = dep(() => OrderScopesHolder(this)); + + late final _mapScopeHolderDep = dep(() => MapScopeHolder()); + + late final _orderNavigationDelegateDep = dep( + () => OrderNavigationDelegate( + parent.routerDelegateDep.get, + _mapScopeHolderDep.get, + _orderScopesHolderDep.get, + ), + ); + + @override + Account get account => data; + + @override + OnlineScopeHolder get onlineScopeHolder => _onlineScopeHolderDep.get; + + @override + OrderScopesHolder get orderScopesHolder => _orderScopesHolderDep.get; + + @override + MapScopeHolder get mapScopeHolder => _mapScopeHolderDep.get; + + @override + OrdersHandler get ordersHandler => ordersStateHolder; + + @override + MapInitializer get mapInitializer => mapScopeHolder; + + @override + OrdersStateHolder get ordersStateHolder => orderScopesHolder; + + @override + void onStartOrderNavigation(Order order, int lastPosition) { + _orderNavigationDelegateDep.get.onOrderNavigation(order, lastPosition); + } +} + +class AccountScopeHolder extends BaseChildDataScopeHolder< + AccountScope, + AccountScopeContainer, + AppScopeContainer, + Account> implements AccountHolder { + AccountScopeHolder(super.parent) + : super( + scopeObservers: [diObserver], + depObservers: [diObserver], + asyncDepObservers: [diObserver], + ); + + @override + AccountScopeContainer createContainer( + AppScopeContainer parent, Account data) => + AccountScopeContainer(parent: parent, data: data); + + @override + Account? get account => scope?.account; + + @override + Stream get accountStream => stream.map((scope) => scope?.account); + + @override + Future setAccount(Account account) async { + await _dropIfNeeded(); + await create(account); + } + + @override + Future dropAccount() async => _dropIfNeeded(); + + Future _dropIfNeeded() async { + if (account != null) { + await drop(); + } + } +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/di/app/app_scope.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/di/app/app_scope.dart new file mode 100644 index 0000000..0e1bb23 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/di/app/app_scope.dart @@ -0,0 +1,50 @@ +import 'package:yx_scope/yx_scope.dart'; + +import '../../domain/auth/auth_manager.dart'; +import '../../router/app_state_observer.dart'; +import '../../router/router_delegate.dart'; +import '../account/account_scope.dart'; +import '../register/register_scope.dart'; +import '../utils/listeners.dart'; + +class AppScopeContainer extends ScopeContainer { + @override + List> get initializeQueue => [ + { + appStateObserverDep, + } + ]; + + late final accountScopeHolderDep = dep(() => AccountScopeHolder(this)); + + late final registerScopeHolderDep = dep(() => RegisterScopeHolder(this)); + + late final authManagerDep = dep( + () => AuthManager( + accountScopeHolderDep.get, + registerScopeHolderDep.get, + ), + ); + + late final routerDelegateDep = dep(() => AppRouterDelegate()); + + late final appStateObserverDep = asyncDep( + () => AppStateObserver( + routerDelegateDep.get, + accountScopeHolderDep.get, + registerScopeHolderDep.get, + ), + ); +} + +class AppScopeHolder extends ScopeHolder { + AppScopeHolder() + : super( + scopeObservers: [diObserver], + depObservers: [diObserver], + asyncDepObservers: [diObserver], + ); + + @override + AppScopeContainer createContainer() => AppScopeContainer(); +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/di/map/map_scope.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/di/map/map_scope.dart new file mode 100644 index 0000000..2e1ce11 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/di/map/map_scope.dart @@ -0,0 +1,67 @@ +import 'package:yx_scope/yx_scope.dart'; + +import '../../data/map/map.dart'; +import '../../domain/map/map_factory.dart'; +import '../../domain/map/map_manager.dart'; +import '../../domain/map_navigation/map_navigation_manager.dart'; +import '../map_navigation/map_navigation_scope.dart'; +import '../utils/listeners.dart'; + +abstract class MapScope implements Scope { + MapNavigationScopeHolder get mapNavigationScopeHolder; + + MapController get controller; + + MapManager get mapManager; + + MapNavigationHolder get mapNavigationHolder; +} + +class MapScopeContainer extends DataScopeContainer + implements MapScope, MapNavigationParent { + MapScopeContainer({required super.data}); + + late final _mapNavigationScopeHolderDep = + dep(() => MapNavigationScopeHolder(this)); + + late final _mapManagerDep = + dep(() => MapManager(data, _mapNavigationScopeHolderDep.get)); + + @override + MapNavigationScopeHolder get mapNavigationScopeHolder => + _mapNavigationScopeHolderDep.get; + + @override + MapManager get mapManager => _mapManagerDep.get; + + @override + MapController get controller => data; + + @override + MapNavigationHolder get mapNavigationHolder => + _mapNavigationScopeHolderDep.get; + + @override + void onNavigationStop() => _mapNavigationScopeHolderDep.get.drop(); +} + +class MapScopeHolder + extends BaseDataScopeHolder + implements MapInitializer { + MapScopeHolder() + : super( + scopeObservers: [diObserver], + depObservers: [diObserver], + asyncDepObservers: [diObserver], + ); + + @override + MapScopeContainer createContainer(MapController data) => + MapScopeContainer(data: data); + + @override + Future createMap(MapController mapController) => create(mapController); + + @override + Future dropMap() => drop(); +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/di/map_navigation/map_navigation_scope.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/di/map_navigation/map_navigation_scope.dart new file mode 100644 index 0000000..55910ec --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/di/map_navigation/map_navigation_scope.dart @@ -0,0 +1,66 @@ +import 'package:yx_scope/yx_scope.dart'; + +import '../../domain/map/map_manager.dart'; +import '../../domain/map_navigation/map_navigation_manager.dart'; +import '../utils/listeners.dart'; + +abstract class MapNavigationScope { + MapNavigationManager get mapNavigationManager; +} + +abstract class MapNavigationParent implements Scope { + MapManager get mapManager; + + void onNavigationStop(); +} + +class MapNavigationScopeContainer + extends ChildDataScopeContainer + implements MapNavigationScope { + MapNavigationScopeContainer({required super.parent, required super.data}); + + @override + List> get initializeQueue => [ + {_mapNavigationManagerDep} + ]; + + late final _mapNavigationManagerDep = rawAsyncDep( + () => MapNavigationManager( + data, + parent.mapManager, + parent.onNavigationStop, + ), + init: (dep) async => dep.prepare(), + dispose: (dep) async {}, + ); + + @override + MapNavigationManager get mapNavigationManager => _mapNavigationManagerDep.get; +} + +class MapNavigationScopeHolder extends BaseChildDataScopeHolder< + MapNavigationScope, + MapNavigationScopeContainer, + MapNavigationParent, + MapNavigationParams> implements MapNavigationHolder { + MapNavigationScopeHolder(super.parent) + : super( + scopeObservers: [diObserver], + depObservers: [diObserver], + asyncDepObservers: [diObserver], + ); + + @override + MapNavigationScopeContainer createContainer( + MapNavigationParent parent, + MapNavigationParams data, + ) => + MapNavigationScopeContainer(parent: parent, data: data); + + @override + Future startNavigation(MapNavigationParams navigationParams) => + create(navigationParams); + + @override + Future stopNavigation() => drop(); +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/di/online_order/online_scope.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/di/online_order/online_scope.dart new file mode 100644 index 0000000..2274da4 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/di/online_order/online_scope.dart @@ -0,0 +1,66 @@ +import 'dart:async'; + +import 'package:yx_scope/yx_scope.dart'; + +import '../../data/orders/incoming_orders_provider.dart'; +import '../../domain/order/accept_order_manager.dart'; +import '../../domain/order/online_order_state_holder.dart'; +import '../../domain/order/orders_state_holder.dart'; +import '../utils/listeners.dart'; + +abstract class OnlineScope implements Scope { + AcceptOrderManager get acceptOrderManager; +} + +abstract class OnlineScopeParent implements Scope { + OrdersStateHolder get ordersStateHolder; +} + +class OnlineScopeContainer extends ChildScopeContainer + implements OnlineScope { + OnlineScopeContainer({required super.parent}); + + @override + List> get initializeQueue => [ + {_acceptOrderManagerDep} + ]; + + late final _incomingOrdersProviderDep = dep(() => IncomingOrdersProvider()); + + late final _acceptOrderManagerDep = asyncDep(() => AcceptOrderManager( + parent.ordersStateHolder, + _incomingOrdersProviderDep.get, + )); + + @override + AcceptOrderManager get acceptOrderManager => _acceptOrderManagerDep.get; +} + +class OnlineScopeHolder extends BaseChildScopeHolder implements OnlineOrderStateHolder { + OnlineScopeHolder(super.parent) + : super( + scopeObservers: [diObserver], + depObservers: [diObserver], + asyncDepObservers: [diObserver], + ); + + @override + OnlineScopeContainer createContainer(OnlineScopeParent parent) => + OnlineScopeContainer(parent: parent); + + @override + Stream get isOnlineStream => stream.map((scope) => scope != null); + + @override + bool get isOnline => scope != null; + + @override + Future toggle() async { + if (scope == null) { + await create(); + return; + } + await drop(); + } +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/di/order/order_scope.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/di/order/order_scope.dart new file mode 100644 index 0000000..4ec2068 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/di/order/order_scope.dart @@ -0,0 +1,113 @@ +import 'dart:async'; + +import 'package:yx_scope/yx_scope.dart'; + +import '../../domain/order/models/order.dart'; +import '../../domain/order/order_manager.dart'; +import '../../domain/order/order_position_manager.dart'; +import '../../domain/order/orders_state_holder.dart'; +import '../utils/listeners.dart'; + +abstract class OrderScope implements Scope { + OrderManager get orderManager; + + OrderPositionHolder get orderPositionHolder; +} + +abstract class OrderScopeParent implements Scope { + OrdersHandler get ordersHandler; + + void onStartOrderNavigation(Order order, int lastPosition); +} + +class OrderScopeContainer + extends ChildDataScopeContainer + implements OrderScope { + OrderScopeContainer({ + required super.parent, + required super.data, + }); + + late final _orderManagerDep = dep( + () => OrderManager( + data, + parent.ordersHandler, + _orderPositionHolderDep.get, + parent.onStartOrderNavigation, + ), + ); + + late final _orderPositionHolderDep = dep(() => OrderPositionHolder(data)); + + @override + OrderManager get orderManager => _orderManagerDep.get; + + @override + OrderPositionHolder get orderPositionHolder => _orderPositionHolderDep.get; +} + +class OrderScopesHolder implements OrdersStateHolder { + final OrderScopeParent parent; + + final _controller = + StreamController>.broadcast(); + + final orderScopes = {}; + + Stream> get orderScopesStream => + _controller.stream; + + OrderScopesHolder(this.parent); + + @override + Future addOrder(Order order) async { + final holder = OrderScopeHolder(parent); + orderScopes[order] = holder; + await holder.create(order); + _notifyListeners(); + } + + @override + Future removeOrder(Order order) async { + await orderScopes.remove(order)?.drop(); + _notifyListeners(); + } + + @override + List get orders => + orderScopes.values.map((e) => e.scope).toList().fold( + [], + (previousValue, scope) { + if (scope != null) { + previousValue.add(scope.orderManager); + } + return previousValue; + }, + ); + + void _notifyListeners() { + _controller.add(orderScopes); + } + + @override + Stream> get ordersStream => + _controller.stream.map((scopes) => scopes.values + .map((holder) => holder.scope) + .whereType() + .map((scope) => scope._orderManagerDep.get) + .toList(growable: false)); +} + +class OrderScopeHolder extends BaseChildDataScopeHolder { + OrderScopeHolder(super.parent) + : super( + scopeObservers: [diObserver], + depObservers: [diObserver], + asyncDepObservers: [diObserver], + ); + + @override + OrderScopeContainer createContainer(OrderScopeParent parent, Order data) => + OrderScopeContainer(parent: parent, data: data); +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/di/register/register_scope.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/di/register/register_scope.dart new file mode 100644 index 0000000..aa1707a --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/di/register/register_scope.dart @@ -0,0 +1,44 @@ +import 'package:yx_scope/yx_scope.dart'; + +import '../../domain/auth/register_manager.dart'; +import '../../domain/auth/register_state_holder.dart'; +import '../app/app_scope.dart'; +import '../utils/listeners.dart'; + +class RegisterScopeContainer extends ChildScopeContainer { + RegisterScopeContainer({required super.parent}); + + late final registerManagerDep = dep( + () => RegisterManager( + parent.accountScopeHolderDep.get, + parent.registerScopeHolderDep.get, + ), + ); +} + +class RegisterScopeHolder + extends ChildScopeHolder + implements RegisterHolder { + RegisterScopeHolder(super.parent) + : super( + scopeObservers: [diObserver], + depObservers: [diObserver], + asyncDepObservers: [diObserver], + ); + + @override + RegisterScopeContainer createContainer(parent) => + RegisterScopeContainer(parent: parent); + + @override + bool get inProgress => scope != null; + + @override + Stream get inProgressStream => stream.map((scope) => scope != null); + + @override + Future startRegister() async => await create(); + + @override + Future stopRegister() async => await drop(); +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/di/utils/listeners.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/di/utils/listeners.dart new file mode 100644 index 0000000..31a9e4e --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/di/utils/listeners.dart @@ -0,0 +1,111 @@ +import 'package:flutter/foundation.dart'; +import 'package:yx_scope/yx_scope.dart'; + +const diObserver = DIObserver(); + +class DIObserver implements ScopeObserver, DepObserver, AsyncDepObserver { + const DIObserver(); + + static void _log( + String message, [ + Object? exception, + StackTrace? stackTrace, + ]) { + if (kDebugMode) { + print(message); + if (exception != null) { + print(exception); + if (stackTrace != null) { + print(stackTrace); + } + } + } + } + + @override + void onScopeStartInitialize(ScopeId scope) => + _log('[$scope] -> onScopeStartInitialize'); + + @override + void onScopeInitialized(ScopeId scope) => + _log('[$scope] -> onScopeInitialized'); + + @override + void onScopeInitializeFailed( + ScopeId scope, + Object exception, + StackTrace stackTrace, + ) => + _log('[$scope] -> onScopeInitializeFailed', exception, stackTrace); + + @override + void onScopeStartDispose(ScopeId scope) => + _log('[$scope] -> onScopeStartDispose'); + + @override + void onScopeDisposed(ScopeId scope) => _log('[$scope] -> onScopeDisposed'); + + @override + void onScopeDisposeDepFailed( + ScopeId scope, + DepId dep, + Object exception, + StackTrace stackTrace, + ) => + _log('[$scope] -> onScopeDisposeDepFailed', exception, stackTrace); + + @override + void onValueStartCreate(ScopeId scope, DepId dep) => + _log('[$scope.$dep] -> onValueStartCreate'); + + @override + void onValueCreated(ScopeId scope, DepId dep, ValueMeta? valueMeta) => + _log('[$scope.$dep] -> onValueCreated'); + + @override + void onValueCreateFailed( + ScopeId scope, + DepId dep, + Object exception, + StackTrace stackTrace, + ) => + _log('[$scope.$dep] -> onValueCreated', exception, stackTrace); + + @override + void onValueCleared(ScopeId scope, DepId dep, ValueMeta? valueMeta) => + _log('[$scope.$dep]($valueMeta) -> onValueCleared'); + + @override + void onDepStartInitialize(ScopeId scope, DepId dep) => + _log('[$scope.$dep] -> onDepStartInitialize'); + + @override + void onDepInitialized(ScopeId scope, DepId dep) => + _log('[$scope.$dep] -> onDepInitialized'); + + @override + void onDepStartDispose(ScopeId scope, DepId dep) => + _log('[$scope.$dep] -> onDepStartDispose'); + + @override + void onDepDisposed(ScopeId scope, DepId dep) => + _log('[$scope.$dep] -> onDepDisposed'); + + @override + void onDepInitializeFailed( + ScopeId scope, + DepId dep, + Object exception, + StackTrace stackTrace, + ) => + _log('[$scope.$dep] -> onDepInitializeFailed', exception, stackTrace); + + @override + void onDepDisposeFailed( + ScopeId scope, + DepId dep, + Object exception, + StackTrace stackTrace, + ) => + _log('[$scope.$dep] -> onDepDisposeFailed', exception, stackTrace); +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/domain/auth/account_holder.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/domain/auth/account_holder.dart new file mode 100644 index 0000000..fcbdfef --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/domain/auth/account_holder.dart @@ -0,0 +1,11 @@ +import 'models/account.dart'; + +abstract class AccountHolder { + Account? get account; + + Stream get accountStream; + + Future setAccount(Account account); + + Future dropAccount(); +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/domain/auth/auth_manager.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/domain/auth/auth_manager.dart new file mode 100644 index 0000000..1e5e7ea --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/domain/auth/auth_manager.dart @@ -0,0 +1,27 @@ +import 'account_holder.dart'; +import 'models/account.dart'; +import 'models/account_params.dart'; +import 'register_state_holder.dart'; + +class AuthManager { + final AccountHolder _accountHolder; + final RegisterHolder registerStateHolder; + + AuthManager(this._accountHolder, this.registerStateHolder); + + Future login(AccountParams loginParams) async { + await _accountHolder.setAccount(Account('123', 'Bob')); + } + + Future logout() async { + await _accountHolder.dropAccount(); + } + + Future startRegister() async { + await registerStateHolder.startRegister(); + } + + Future stopRegister() async { + await registerStateHolder.stopRegister(); + } +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/domain/auth/models/account.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/domain/auth/models/account.dart new file mode 100644 index 0000000..e3b37d2 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/domain/auth/models/account.dart @@ -0,0 +1,6 @@ +class Account { + final String uid; + final String name; + + Account(this.uid, this.name); +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/domain/auth/models/account_params.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/domain/auth/models/account_params.dart new file mode 100644 index 0000000..7047c3b --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/domain/auth/models/account_params.dart @@ -0,0 +1,6 @@ +class AccountParams { + final String login; + final String password; + + AccountParams(this.login, this.password); +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/domain/auth/register_manager.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/domain/auth/register_manager.dart new file mode 100644 index 0000000..dda9dcf --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/domain/auth/register_manager.dart @@ -0,0 +1,15 @@ +import 'account_holder.dart'; +import 'models/account.dart'; +import 'register_state_holder.dart'; + +class RegisterManager { + final AccountHolder _accountHolder; + final RegisterHolder _registerStateHolder; + + RegisterManager(this._accountHolder, this._registerStateHolder); + + Future createAccount() async { + await _accountHolder.setAccount(Account('321', 'New Bob')); + await _registerStateHolder.stopRegister(); + } +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/domain/auth/register_state_holder.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/domain/auth/register_state_holder.dart new file mode 100644 index 0000000..3da8e27 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/domain/auth/register_state_holder.dart @@ -0,0 +1,9 @@ +abstract class RegisterHolder { + bool get inProgress; + + Stream get inProgressStream; + + Future startRegister(); + + Future stopRegister(); +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/domain/map/map_factory.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/domain/map/map_factory.dart new file mode 100644 index 0000000..3ae3752 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/domain/map/map_factory.dart @@ -0,0 +1,7 @@ +import '../../data/map/map.dart'; + +abstract class MapInitializer { + Future createMap(MapController mapController); + + Future dropMap(); +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/domain/map/map_manager.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/domain/map/map_manager.dart new file mode 100644 index 0000000..b1b8274 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/domain/map/map_manager.dart @@ -0,0 +1,46 @@ +import '../../data/map/map.dart'; + +import '../../data/orders/models/incoming_order_data.dart'; +import '../map_navigation/map_navigation_manager.dart'; + +class MapManager { + final MapController _mapController; + final MapNavigationHolder _mapNavigationHolder; + + const MapManager( + this._mapController, + this._mapNavigationHolder, + ); + + List get selectedItems => _mapController.items.values + .where((item) => item.selected) + .toList(growable: false); + + void focus(int position) { + _mapController.focus(position); + } + + void selectMapItem(int position, {required bool selected}) { + _mapController.selectPosition(position, selected: selected); + } + + void clear() { + _mapController.clear(); + } + + void startNavigation() { + assert( + selectedItems.length > 1, + 'Incorrect number of selected items for map navigation', + ); + _mapNavigationHolder.startNavigation( + MapNavigationParams( + fromAddress: + Address(name: 'Street', position: selectedItems[0].position), + toAddress: Address( + name: 'Street', + position: selectedItems[selectedItems.length - 1].position), + ), + ); + } +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/domain/map_navigation/map_navigation_manager.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/domain/map_navigation/map_navigation_manager.dart new file mode 100644 index 0000000..275cb66 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/domain/map_navigation/map_navigation_manager.dart @@ -0,0 +1,102 @@ +import 'dart:async'; + +import '../../data/orders/models/incoming_order_data.dart'; +import '../map/map_manager.dart'; + +class MapNavigationParams { + final Address fromAddress; + final Address toAddress; + final int? currentPosition; + + MapNavigationParams({ + required this.fromAddress, + required this.toAddress, + this.currentPosition, + }); + + MapNavigationParams copyWith({ + int? currentPosition, + }) { + return MapNavigationParams( + fromAddress: fromAddress, + toAddress: toAddress, + currentPosition: currentPosition ?? this.currentPosition, + ); + } +} + +typedef OnStopNavigation = void Function(); + +abstract class MapNavigationHolder { + Future startNavigation(MapNavigationParams navigationParams); + + Future stopNavigation(); +} + +class MapNavigationManager { + final _controller = StreamController.broadcast(); + + Stream get stream => _controller.stream; + + final MapManager _mapManager; + final OnStopNavigation _onStopNavigation; + MapNavigationParams _current; + + MapNavigationManager( + this._current, + this._mapManager, + this._onStopNavigation, + ); + + void prepare() { + _mapManager.selectMapItem(_current.fromAddress.position, selected: true); + final currentPosition = _current.currentPosition; + if (currentPosition != null) { + final sign = _sign(_current); + for (var i = _current.fromAddress.position; + sign > 0 ? i <= currentPosition : i >= currentPosition; + i += sign) { + _mapManager.selectMapItem(i, selected: true); + } + } + _mapManager.selectMapItem(_current.toAddress.position, selected: true); + if (currentPosition != null) { + _mapManager.focus(currentPosition); + } else { + _mapManager.focus(_current.fromAddress.position); + } + } + + void navigateToNextPosition() { + final sign = _sign(_current); + final currentPosition = + _current.currentPosition ?? _current.fromAddress.position; + + final nextPosition = currentPosition + sign; + if (_current.toAddress.position - nextPosition == -sign) { + // reached the destination + stopNavigation(); + } else { + _mapManager.focus(nextPosition); + _emit(_current.copyWith(currentPosition: nextPosition)); + } + } + + int _sign(MapNavigationParams params) { + final direction = params.toAddress.position - params.fromAddress.position; + return direction ~/ direction.abs(); + } + + Future stopNavigation() async => _onStopNavigation(); + + void _emit(MapNavigationParams params) { + _current = params; + _controller.add(params); + final currentPosition = params.currentPosition; + if (currentPosition != null) { + _mapManager.selectMapItem(currentPosition, selected: true); + } else { + _mapManager.clear(); + } + } +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/domain/order/accept_order_manager.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/domain/order/accept_order_manager.dart new file mode 100644 index 0000000..fa8d500 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/domain/order/accept_order_manager.dart @@ -0,0 +1,69 @@ +import 'dart:async'; + +import 'package:yx_scope/yx_scope.dart'; + +import '../../data/orders/incoming_orders_provider.dart'; +import 'models/order.dart'; +import 'orders_state_holder.dart'; + +/// This manager listens for incoming orders. +/// Has a [_maxCountOfOrders] limit. +class AcceptOrderManager implements AsyncLifecycle { + static const _maxCountOfOrders = 3; + + final OrdersStateHolder _ordersStateHolder; + final IncomingOrdersProvider _incomingOrdersProvider; + + final _controller = StreamController.broadcast(); + late IncomingOrderSession _session; + StreamSubscription? _maxCountOfOrdersSub; + + AcceptOrderManager( + this._ordersStateHolder, + this._incomingOrdersProvider, + ); + + Stream get toAcceptOrdersStream => _controller.stream; + + Future acceptOrder(Order order) async { + await _ordersStateHolder.addOrder(order); + _resumeIfNeeded(); + } + + Future cancelOrder() async { + _resumeIfNeeded(); + } + + @override + Future init() async { + _session = _incomingOrdersProvider.incomingOrderSession; + _session.incomingOrdersStream.listen((incomingOrder) { + _controller.add(Order(incomingOrder)); + _session.pause(); + }); + _maxCountOfOrdersSub = _ordersStateHolder.ordersStream + .map((orders) => orders.length) + .listen((countOfOrders) { + if (countOfOrders >= _maxCountOfOrders) { + _session.pause(); + } else { + _resumeIfNeeded(); + } + }); + } + + @override + Future dispose() async { + await _controller.close(); + await _maxCountOfOrdersSub?.cancel(); + await _session.dispose(); + } + + void _resumeIfNeeded() { + final session = _session; + if (session.isPaused && + _ordersStateHolder.orders.length < _maxCountOfOrders) { + session.resume(); + } + } +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/domain/order/models/order.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/domain/order/models/order.dart new file mode 100644 index 0000000..e68d7ef --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/domain/order/models/order.dart @@ -0,0 +1,9 @@ +import '../../../data/orders/models/incoming_order_data.dart'; + +class Order { + final IncomingOrder incomingOrder; + + Order(this.incomingOrder); + + String get uid => incomingOrder.uid; +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/domain/order/online_order_state_holder.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/domain/order/online_order_state_holder.dart new file mode 100644 index 0000000..dbe64e0 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/domain/order/online_order_state_holder.dart @@ -0,0 +1,5 @@ +abstract class OnlineOrderStateHolder { + bool get isOnline; + Stream get isOnlineStream; + Future toggle(); +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/domain/order/order_manager.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/domain/order/order_manager.dart new file mode 100644 index 0000000..b67ee00 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/domain/order/order_manager.dart @@ -0,0 +1,25 @@ +import 'models/order.dart'; +import 'order_position_manager.dart'; +import 'orders_state_holder.dart'; + +typedef CancelOrderListener = void Function(); + +class OrderManager { + final Order order; + final OrdersHandler _ordersHandler; + final OrderPositionHolder _orderPositionHolder; + final void Function(Order order, int lastPosition) _onOrderNavigation; + + OrderManager( + this.order, + this._ordersHandler, + this._orderPositionHolder, + this._onOrderNavigation, + ); + + void goToMapNavigation() { + _onOrderNavigation(order, _orderPositionHolder.position); + } + + void cancelOrder() => _ordersHandler.removeOrder(order); +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/domain/order/order_position_manager.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/domain/order/order_position_manager.dart new file mode 100644 index 0000000..bcb69c3 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/domain/order/order_position_manager.dart @@ -0,0 +1,10 @@ +import 'models/order.dart'; + +class OrderPositionHolder { + int position; + + OrderPositionHolder(Order order) + : position = order.incomingOrder.fromAddress.position; + + void updatePosition(int position) => this.position = position; +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/domain/order/orders_state_holder.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/domain/order/orders_state_holder.dart new file mode 100644 index 0000000..5d808bf --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/domain/order/orders_state_holder.dart @@ -0,0 +1,16 @@ +import 'models/order.dart'; +import 'order_manager.dart'; + +abstract class OrdersStateHolder implements OrdersStates, OrdersHandler {} + +abstract class OrdersStates { + Stream> get ordersStream; + + List get orders; +} + +abstract class OrdersHandler { + Future addOrder(Order order); + + Future removeOrder(Order order); +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/domain/order_navigation/order_navigation_delegate.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/domain/order_navigation/order_navigation_delegate.dart new file mode 100644 index 0000000..131e28b --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/domain/order_navigation/order_navigation_delegate.dart @@ -0,0 +1,67 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; + +import '../../di/map/map_scope.dart'; +import '../../di/map_navigation/map_navigation_scope.dart'; +import '../../di/order/order_scope.dart'; +import '../../domain/map_navigation/map_navigation_manager.dart'; +import '../../domain/order/models/order.dart'; +import '../../router/models/app_state.dart'; +import '../../router/router_delegate.dart'; + +class OrderNavigationDelegate { + final AppRouterDelegate _appRouterDelegate; + final MapScopeHolder _mapScopeHolder; + final OrderScopesHolder _orderScopesHolder; + + StreamSubscription? _subscription; + + OrderNavigationDelegate( + this._appRouterDelegate, + this._mapScopeHolder, + this._orderScopesHolder, + ); + + Future onOrderNavigation(Order order, int lastPosition) async { + if (_appRouterDelegate.state.tabbarPage != TabbarPageType.map) { + _appRouterDelegate.setTabBarPage(TabbarPageType.map); + } + _subscription = _mapScopeHolder.stream + .asyncExpand((scope) => scope == null + ? Stream.value(null) + : scope.mapNavigationScopeHolder.stream) + .asyncExpand((scope) => scope == null + ? Stream.value(null) + : scope.mapNavigationManager.stream) + .listen((params) { + final position = params?.currentPosition; + if (position != null) { + _orderScopesHolder.orderScopes[order]?.scope?.orderPositionHolder + .updatePosition(position); + _log('Order position updated: $position'); + } else { + _log('Order position update canceled'); + _subscription?.cancel(); + } + }); + + // wait for MapScope appears + final mapScope = await _mapScopeHolder.stream + .firstWhere((scope) => scope != null) as MapScope; + + await mapScope.mapNavigationHolder.startNavigation( + MapNavigationParams( + fromAddress: order.incomingOrder.fromAddress, + toAddress: order.incomingOrder.toAddress, + currentPosition: lastPosition, + ), + ); + } + + void _log(String message) { + if (kDebugMode) { + print(message); + } + } +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/main.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/main.dart new file mode 100644 index 0000000..f2a1e3c --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/main.dart @@ -0,0 +1,55 @@ +import 'package:flutter/material.dart'; +import 'package:yx_scope/yx_scope.dart'; +import 'package:yx_scope_flutter/yx_scope_flutter.dart'; + +import 'di/app/app_scope.dart'; +import 'utils/logger.dart'; + +Future main() async { + ScopeObservatory.logger = const Logger(); + runApp(const App()); +} + +class App extends StatefulWidget { + const App({super.key}); + + @override + State createState() => _AppState(); +} + +class _AppState extends State { + final _appScopeHolder = AppScopeHolder(); + + @override + void initState() { + super.initState(); + _appScopeHolder.create(); + } + + @override + void dispose() { + _appScopeHolder.drop(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return ScopeProvider( + holder: _appScopeHolder, + child: ScopeBuilder.withPlaceholder( + builder: (context, appScope) { + return MaterialApp.router( + title: 'YxScopedFlutter Demo', + theme: ThemeData( + primarySwatch: Colors.blue, + ), + routerDelegate: appScope.routerDelegateDep.get, + ); + }, + + // Shows this widget while [appScopeHolder] is loading + placeholder: const Center(child: CircularProgressIndicator()), + ), + ); + } +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/router/app_state_observer.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/router/app_state_observer.dart new file mode 100644 index 0000000..f150783 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/router/app_state_observer.dart @@ -0,0 +1,41 @@ +import 'dart:async'; + +import 'package:yx_scope/yx_scope.dart'; + +import '../domain/auth/account_holder.dart'; +import '../domain/auth/register_state_holder.dart'; +import 'router_delegate.dart'; + +class AppStateObserver implements AsyncLifecycle { + final AppRouterDelegate _routerDelegate; + final AccountHolder _accountHolder; + final RegisterHolder _registerHolder; + + late StreamSubscription _accountSubscription; + late StreamSubscription _registerSubscription; + + AppStateObserver( + this._routerDelegate, + this._accountHolder, + this._registerHolder, + ); + + @override + Future init() async { + _accountSubscription = _accountHolder.accountStream.listen((account) { + _routerDelegate.setHasAccount(hasAccount: account != null); + }); + _registerSubscription = + _registerHolder.inProgressStream.listen((inProgress) { + _routerDelegate.setOpenRegister(isOpenRegister: inProgress); + }); + } + + @override + Future dispose() async { + await Future.wait([ + _accountSubscription.cancel(), + _registerSubscription.cancel(), + ]); + } +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/router/models/app_state.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/router/models/app_state.dart new file mode 100644 index 0000000..770edf6 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/router/models/app_state.dart @@ -0,0 +1,33 @@ +enum TabbarPageType { + map, + orders, + account; +} + +class AppState { + final bool hasAccount; + final bool isOpenRegister; + final TabbarPageType tabbarPage; + + const AppState._( + this.hasAccount, + this.isOpenRegister, + this.tabbarPage, + ); + + const AppState.init() + : hasAccount = false, + isOpenRegister = false, + tabbarPage = TabbarPageType.orders; + + AppState copyWith({ + bool? hasAccount, + bool? isOpenRegister, + TabbarPageType? tabbarPage, + }) => + AppState._( + hasAccount ?? this.hasAccount, + isOpenRegister ?? this.isOpenRegister, + tabbarPage ?? this.tabbarPage, + ); +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/router/router_delegate.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/router/router_delegate.dart new file mode 100644 index 0000000..4442669 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/router/router_delegate.dart @@ -0,0 +1,82 @@ +import 'package:flutter/material.dart'; +import 'package:yx_scope_flutter/yx_scope_flutter.dart'; + +import '../di/app/app_scope.dart'; +import '../ui/auth/auth_page.dart'; +import '../ui/register/register_page.dart'; +import '../ui/tabbar/tabbar_page.dart'; +import 'models/app_state.dart'; + +class AppRouterDelegate extends RouterDelegate + with ChangeNotifier, PopNavigatorRouterDelegateMixin { + var state = const AppState.init(); + @override + late final navigatorKey = GlobalKey(); + + @override + Widget build(BuildContext context) { + final pages = []; + if (!state.hasAccount) { + pages.add( + const MaterialPage( + key: ValueKey('LoginPage'), + child: AuthPage(), + ), + ); + } else { + pages.add( + MaterialPage( + key: const ValueKey('TabBarPage'), + child: ScopeBuilder.withPlaceholder( + builder: (context, appScope) { + return ScopeProvider( + holder: appScope.accountScopeHolderDep.get, + child: TabbarPage(page: state.tabbarPage), + ); + }, + ), + ), + ); + } + + if (state.isOpenRegister) { + pages.add( + const MaterialPage( + key: ValueKey('RegisterPage'), + child: RegisterPage(), + ), + ); + } + return Navigator( + key: navigatorKey, + pages: pages, + // TODO: migrate to the new API + // ignore: deprecated_member_use + onPopPage: (route, result) { + if (!route.didPop(result)) { + return false; + } + + return true; + }, + ); + } + + @override + Future setNewRoutePath(AppState configuration) async {} + + void setHasAccount({required bool hasAccount}) { + state = state.copyWith(hasAccount: hasAccount); + notifyListeners(); + } + + void setOpenRegister({required bool isOpenRegister}) { + state = state.copyWith(isOpenRegister: isOpenRegister); + notifyListeners(); + } + + void setTabBarPage(TabbarPageType page) { + state = state.copyWith(tabbarPage: page); + notifyListeners(); + } +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/ui/account/account_page.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/ui/account/account_page.dart new file mode 100644 index 0000000..23c34a7 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/ui/account/account_page.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; +import 'package:yx_scope_flutter/yx_scope_flutter.dart'; + +import '../../di/account/account_scope.dart'; +import '../../di/app/app_scope.dart'; + +class AccountPage extends StatefulWidget { + const AccountPage({super.key}); + + @override + State createState() => _AccountPageState(); +} + +class _AccountPageState extends State { + @override + Widget build(BuildContext context) => + ScopeBuilder.withPlaceholder( + builder: (context, appScope) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ScopeBuilder( + builder: (context, scope) { + return Text('account: ${scope?.account.name}'); + }, + ), + TextButton( + onPressed: () { + appScope.authManagerDep.get.logout(); + }, + child: const Text('logout'), + ), + TextButton( + onPressed: () { + appScope.authManagerDep.get.startRegister(); + }, + child: const Text('start register'), + ), + ], + ), + ); + }, + ); +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/ui/auth/auth_page.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/ui/auth/auth_page.dart new file mode 100644 index 0000000..b774dd4 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/ui/auth/auth_page.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; +import 'package:yx_scope_flutter/yx_scope_flutter.dart'; + +import '../../di/app/app_scope.dart'; +import '../../domain/auth/models/account_params.dart'; + +class AuthPage extends StatefulWidget { + const AuthPage({super.key}); + + @override + State createState() => _AuthPageState(); +} + +class _AuthPageState extends State { + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar(title: const Text('login')), + body: ScopeBuilder.withPlaceholder( + builder: (context, appScope) { + final authManager = appScope.authManagerDep.get; + + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text('Login'), + TextButton( + onPressed: () => + authManager.login(AccountParams('login', 'pass')), + child: const Text('login'), + ), + TextButton( + onPressed: () => authManager.startRegister(), + child: const Text('start register'), + ), + ], + ), + ); + }, + ), + ); +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/ui/map/map_page.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/ui/map/map_page.dart new file mode 100644 index 0000000..8f9642e --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/ui/map/map_page.dart @@ -0,0 +1,87 @@ +import 'package:flutter/material.dart'; +import 'package:yx_scope_flutter/yx_scope_flutter.dart'; + +import '../../data/map/map.dart'; +import '../../data/map/map_widget.dart'; +import '../../di/account/account_scope.dart'; +import '../../di/map/map_scope.dart'; +import '../../di/map_navigation/map_navigation_scope.dart'; +import '../../domain/map/map_factory.dart'; + +const kMainMap = 'main_map_page_key'; + +class MapPage extends StatefulWidget { + const MapPage({super.key}); + + @override + State createState() => _MapPageState(); +} + +class _MapPageState extends State { + late final AccountScope _accountScope; + late final MapInitializer _mapInitializer; + + void _onMapCreated(MapController map) { + _mapInitializer.createMap(map); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + _accountScope = ScopeProvider.of(context)!; + _mapInitializer = _accountScope.mapInitializer; + } + + @override + void dispose() { + super.dispose(); + _mapInitializer.dropMap(); + } + + @override + Widget build(BuildContext context) => Scaffold( + body: Column( + children: [ + const Center( + child: Text('Tap a street to select points for navigation'), + ), + Expanded(child: MapWidget(onMapCreated: _onMapCreated)), + ], + ), + floatingActionButton: ScopeBuilder.withPlaceholder( + holder: _accountScope.mapScopeHolder, + builder: (context, mapScope) { + return ListenableBuilder( + listenable: mapScope.controller, + builder: (context, value) { + if (mapScope.mapManager.selectedItems.length > 1) { + return ScopeBuilder( + holder: mapScope.mapNavigationScopeHolder, + builder: (context, navigationScope) { + if (navigationScope == null) { + return FloatingActionButton.extended( + onPressed: () { + mapScope.mapManager.startNavigation(); + }, + label: const Text('Start Navigation'), + ); + } else { + return FloatingActionButton.extended( + onPressed: () { + navigationScope.mapNavigationManager + .navigateToNextPosition(); + }, + label: const Text('Proceed to next point'), + ); + } + }, + ); + } else { + return const SizedBox.shrink(); + } + }); + }, + ), + floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat, + ); +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/ui/orders/accept_order_wrapper.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/ui/orders/accept_order_wrapper.dart new file mode 100644 index 0000000..845202d --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/ui/orders/accept_order_wrapper.dart @@ -0,0 +1,96 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:yx_scope_flutter/yx_scope_flutter.dart'; + +import '../../di/account/account_scope.dart'; +import '../../di/online_order/online_scope.dart'; +import '../../domain/order/models/order.dart'; + +class AcceptOrderWrapper extends StatefulWidget { + final Widget child; + + const AcceptOrderWrapper({ + required this.child, + Key? key, + }) : super(key: key); + + @override + State createState() => _AcceptOrderWrapperState(); +} + +class _AcceptOrderWrapperState extends State { + StreamSubscription? _acceptOrdersSub; + + @override + void dispose() { + super.dispose(); + _acceptOrdersSub?.cancel(); + _acceptOrdersSub = null; + } + + void _listener( + BuildContext _, + OnlineScope? onlineScope, + ) { + if (onlineScope != null) { + final acceptOrderManger = onlineScope.acceptOrderManager; + + _acceptOrdersSub?.cancel(); + _acceptOrdersSub = acceptOrderManger.toAcceptOrdersStream.listen((order) { + if (!mounted) { + return; + } + _handleIncomingOrder(onlineScope, order); + }); + return; + } + + _acceptOrdersSub?.cancel(); + _acceptOrdersSub = null; + } + + Future _handleIncomingOrder( + OnlineScope onlineScope, + Order order, + ) async { + final acceptOrderManager = onlineScope.acceptOrderManager; + final result = await showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text( + 'Accept oder ${order.incomingOrder.fromAddress.name} -> ${order.incomingOrder.toAddress.name}', + ), + actions: [ + TextButton( + onPressed: () { + acceptOrderManager.cancelOrder(); + Navigator.of(context).pop(false); + }, + child: const Text('no'), + ), + TextButton( + onPressed: () { + acceptOrderManager.acceptOrder(order); + Navigator.of(context).pop(true); + }, + child: const Text('ok'), + ), + ], + ), + ); + if (result == null) { + await acceptOrderManager.cancelOrder(); + } + } + + @override + Widget build(BuildContext context) => + ScopeBuilder.withPlaceholder( + builder: (context, accountScope) => ScopeListener( + holder: accountScope.onlineScopeHolder, + listener: _listener, + child: widget.child, + ), + ); +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/ui/orders/orders_page.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/ui/orders/orders_page.dart new file mode 100644 index 0000000..89be1aa --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/ui/orders/orders_page.dart @@ -0,0 +1,111 @@ +import 'package:flutter/material.dart'; +import 'package:yx_scope_flutter/yx_scope_flutter.dart'; + +import '../../di/account/account_scope.dart'; +import '../../di/order/order_scope.dart'; + +class OrdersPage extends StatefulWidget { + const OrdersPage({super.key}); + + @override + State createState() => _OrdersPageState(); +} + +class _OrdersPageState extends State { + @override + Widget build(BuildContext context) { + return Center( + child: ScopeBuilder.withPlaceholder( + builder: (context, scope) { + final onlineStateHolder = scope.onlineScopeHolder; + return Flex( + direction: Axis.vertical, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Flexible( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + StreamBuilder( + initialData: onlineStateHolder.isOnline, + stream: onlineStateHolder.isOnlineStream, + builder: (context, snap) { + final isOnline = snap.requireData; + return Column( + children: [ + TextButton( + onPressed: () => onlineStateHolder.toggle(), + child: Text( + isOnline ? 'Go Offline' : 'Go Online', + ), + ), + Text( + isOnline ? 'Wait for orders' : 'You are offline', + ) + ], + ); + }, + ), + ], + ), + ), + Flexible( + flex: 2, + child: StreamBuilder( + initialData: scope.orderScopesHolder.orderScopes, + stream: scope.orderScopesHolder.orderScopesStream, + builder: (context, snap) { + final data = snap.data; + if (data == null) { + return const SizedBox.shrink(); + } + + final orderList = data.values.toList(growable: false); + + return ListView.builder( + itemCount: orderList.length, + itemBuilder: (context, index) { + final orderScopeHolder = orderList[index]; + return ScopeBuilder.withPlaceholder( + holder: orderScopeHolder, + builder: (context, orderScope) { + final manager = orderScope.orderManager; + final order = manager.order; + return ListTile( + title: Text( + '${order.incomingOrder.fromAddress.name}' + ' -> ${order.incomingOrder.toAddress.name}', + ), + subtitle: Text( + 'uid: ${order.uid}', + ), + trailing: Row( + mainAxisAlignment: MainAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + TextButton( + onPressed: () => + manager.goToMapNavigation(), + child: const Text('Navigate'), + ), + TextButton( + onPressed: () => manager.cancelOrder(), + child: const Text('Cancel'), + ), + ], + ), + ); + }); + }, + ); + }, + ), + ), + ], + ); + }, + ), + ); + } +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/ui/register/register_page.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/ui/register/register_page.dart new file mode 100644 index 0000000..a8fc37a --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/ui/register/register_page.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; +import 'package:yx_scope_flutter/yx_scope_flutter.dart'; + +import '../../di/app/app_scope.dart'; + +class RegisterPage extends StatefulWidget { + const RegisterPage({super.key}); + + @override + State createState() => _RegisterPageState(); +} + +class _RegisterPageState extends State { + @override + Widget build(BuildContext context) => + ScopeBuilder.withPlaceholder( + builder: (context, appScope) => Scaffold( + appBar: AppBar( + title: const Text('Register'), + leading: IconButton( + onPressed: () { + appScope.authManagerDep.get.stopRegister(); + }, + icon: const Icon(Icons.arrow_back), + ), + ), + body: ScopeBuilder.withPlaceholder( + holder: appScope.registerScopeHolderDep.get, + builder: (context, registerScope) => Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text('Register New Bob'), + TextButton( + onPressed: () { + registerScope.registerManagerDep.get.createAccount(); + }, + child: const Text('Create'), + ), + ], + ), + ), + ), + ), + ); +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/ui/tabbar/tabbar_page.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/ui/tabbar/tabbar_page.dart new file mode 100644 index 0000000..293a323 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/ui/tabbar/tabbar_page.dart @@ -0,0 +1,92 @@ +import 'package:flutter/material.dart'; +import 'package:yx_scope_flutter/yx_scope_flutter.dart'; + +import '../../di/app/app_scope.dart'; +import '../../router/models/app_state.dart'; +import '../account/account_page.dart'; +import '../map/map_page.dart'; +import '../orders/accept_order_wrapper.dart'; +import '../orders/orders_page.dart'; + +class TabbarPage extends StatefulWidget { + final TabbarPageType page; + + const TabbarPage({super.key, required this.page}); + + @override + State createState() => _TabbarPageState(); +} + +class _TabbarPageState extends State { + static const _orderOfPages = [ + TabbarPageType.map, + TabbarPageType.orders, + TabbarPageType.account, + ]; + + @override + Widget build(BuildContext context) => + ScopeBuilder.withPlaceholder( + builder: (context, appScope) { + final routerDelegate = appScope.routerDelegateDep.get; + late final Widget body; + + switch (widget.page) { + case TabbarPageType.map: + body = const MapPage(); + break; + case TabbarPageType.orders: + body = const OrdersPage(); + break; + case TabbarPageType.account: + body = const AccountPage(); + break; + } + return AcceptOrderWrapper( + child: Scaffold( + appBar: AppBar(title: Text(_labelFromType(widget.page))), + body: body, + bottomNavigationBar: NavigationBar( + selectedIndex: widget.page.index, + onDestinationSelected: (index) { + final type = _orderOfPages[index]; + routerDelegate.setTabBarPage(type); + }, + destinations: _destinations, + ), + ), + ); + }, + ); + + List get _destinations => _orderOfPages + .map( + (type) => NavigationDestination( + icon: _iconFromType(type), + label: _labelFromType(type), + ), + ) + .toList(); + + String _labelFromType(TabbarPageType type) { + switch (type) { + case TabbarPageType.map: + return 'Map'; + case TabbarPageType.orders: + return 'Orders'; + case TabbarPageType.account: + return 'Account'; + } + } + + Icon _iconFromType(TabbarPageType type) { + switch (type) { + case TabbarPageType.map: + return const Icon(Icons.map); + case TabbarPageType.orders: + return const Icon(Icons.list); + case TabbarPageType.account: + return const Icon(Icons.account_box_outlined); + } + } +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/utils/logger.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/utils/logger.dart new file mode 100644 index 0000000..e17e494 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/lib/utils/logger.dart @@ -0,0 +1,21 @@ +import 'package:yx_scope/yx_scope.dart'; + +class Logger extends ScopeLogger { + const Logger(); + + @override + void log( + LogType type, + Object message, { + Object? exception, + StackTrace? stackTrace, + }) { + if (type != LogType.error) { + // ignore: avoid_print + print('[INTERNAL] ${type.name}: $message'); + } else { + // ignore: avoid_print + print('[ERROR]: $message\n$exception\n$stackTrace'); + } + } +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/pubspec.yaml b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/pubspec.yaml new file mode 100644 index 0000000..7f626cf --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/pubspec.yaml @@ -0,0 +1,25 @@ +name: yx_scope_flutter_example +description: A new Flutter project. +version: 1.0.0+1 +publish_to: 'none' + +environment: + sdk: '>=2.19.6 <3.0.0' + +dependencies: + flutter: + sdk: flutter + cupertino_icons: ^1.0.2 + yx_scope_flutter: + path: ../ + yx_scope: ^1.0.0 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^2.0.0 + custom_lint: ^0.6.8 + yx_scope_linter: ^0.1.0 + +flutter: + uses-material-design: true diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/pubspec_overrides.yaml b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/pubspec_overrides.yaml new file mode 100644 index 0000000..cb9eff7 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/example/pubspec_overrides.yaml @@ -0,0 +1,6 @@ +dependency_overrides: + yx_scope: + path: ../../yx_scope + yx_scope_linter: + path: ../../yx_scope_linter + hotreloader: 4.3.0 diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/lib/src/core/provider.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/lib/src/core/provider.dart new file mode 100644 index 0000000..ae9aebf --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/lib/src/core/provider.dart @@ -0,0 +1,29 @@ +import 'package:flutter/widgets.dart'; + +class Provider extends InheritedWidget { + final T data; + const Provider({ + required super.child, + required this.data, + super.key, + }); + + static T of(BuildContext context, {bool listen = true}) { + final provider = listen + ? context.dependOnInheritedWidgetOfExactType>() + : (context + .getElementForInheritedWidgetOfExactType>() + ?.widget as Provider?); + + if (provider == null) { + throw NotFoundProviderException(); + } else { + return provider.data; + } + } + + @override + bool updateShouldNotify(Provider oldWidget) => oldWidget.data != data; +} + +class NotFoundProviderException implements Exception {} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/lib/src/core/scope_error.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/lib/src/core/scope_error.dart new file mode 100644 index 0000000..bb4d22b --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/lib/src/core/scope_error.dart @@ -0,0 +1,20 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; + +// This exception must be throw in cases when +// a consumer of this package uses it +// in some inappropriate way. +class FlutterScopeError extends FlutterError { + FlutterScopeError.fromParts(List diagnostics) + : super.fromParts(diagnostics); + + factory FlutterScopeError(String message) { + final List lines = message.split('\n'); + return FlutterScopeError.fromParts([ + ErrorSummary(lines.first), + ...lines + .skip(1) + .map((String line) => ErrorDescription(line)), + ]); + } +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/lib/src/scope_builder.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/lib/src/scope_builder.dart new file mode 100644 index 0000000..44e4d89 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/lib/src/scope_builder.dart @@ -0,0 +1,107 @@ +import 'package:flutter/widgets.dart'; +import 'package:yx_scope/yx_scope.dart' hide ScopeListener; + +import '../yx_scope_flutter.dart'; + +/// [ScopeBuilder] handles building a widget in response to `scope`. +/// +/// If you just want to listen a scope, please refer to [ScopeListener] +/// +/// If the [holder] is omited,[ScopeBuilder] will automatically +/// perform a lookup using [ScopeProvider] and the current [BuildContext]. +/// +/// ```dart +/// ScopeBuilder( +/// builder: (context, scope) { +/// // return widget here based on the scope +/// } +/// ) +/// ``` +/// +/// Only specify the [holder] if you wish to provide a [holder] that is otherwise +/// not accessible via [ScopeProvider] and the current [BuildContext]. +/// +/// ```dart +/// ScopeBuilder( +/// holder: holder, +/// builder: (context, scope) { +/// // return widget here based on the scope +/// } +/// ) +/// ``` +class ScopeBuilder extends StatefulWidget { + final ScopeStateHolder? holder; + final ScopeWidgetBuilder builder; + + const ScopeBuilder({ + required this.builder, + this.holder, + super.key, + }); + + /// This is a factory that provides a non-null scope + /// in the builder and uses a [placeholder] if the scope is null. + factory ScopeBuilder.withPlaceholder({ + required NonNullableScopeWidgetBuilder builder, + Widget placeholder = const SizedBox.shrink(), + ScopeStateHolder? holder, + }) => + ScopeBuilder( + holder: holder, + builder: (context, scope) { + if (scope == null) { + return placeholder; + } + return builder(context, scope); + }, + ); + + @override + State createState() => _ScopeBuilderState(); +} + +class _ScopeBuilderState extends State> { + late ScopeStateHolder _holder; + T? _scope; + + @override + void initState() { + super.initState(); + _holder = widget.holder ?? + ScopeProvider.scopeHolderOf( + context, + listen: false, + ); + _scope = _holder.scope; + } + + @override + void didUpdateWidget(covariant ScopeBuilder oldWidget) { + super.didUpdateWidget(oldWidget); + final currentHolder = + widget.holder ?? ScopeProvider.scopeHolderOf(context, listen: false); + if (_holder != currentHolder) { + _holder = currentHolder; + _scope = _holder.scope; + } + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + final holder = widget.holder ?? ScopeProvider.scopeHolderOf(context); + if (holder != _holder) { + _holder = holder; + _scope = _holder.scope; + } + } + + @override + Widget build(BuildContext context) => ScopeListener( + holder: _holder, + listener: (context, scope) => setState(() { + _scope = scope; + }), + child: widget.builder(context, _scope), + ); +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/lib/src/scope_consumer.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/lib/src/scope_consumer.dart new file mode 100644 index 0000000..d38568c --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/lib/src/scope_consumer.dart @@ -0,0 +1,119 @@ +import 'package:flutter/widgets.dart'; +import 'package:yx_scope/yx_scope.dart' hide ScopeListener; + +import '../yx_scope_flutter.dart'; +import 'scope_widget_listener.dart'; + +/// [ScopeConsumer] exposes a [builder] and [listener] in order react to new +/// scope. +/// [ScopeConsumer] is analogous to a nested [ScopeListener] +/// and [ScopeBuilder] but reduces the amount of boilerplate needed. +/// [ScopeConsumer] should only be used when it is necessary to both rebuild UI +/// and execute other reactions to scope changes in the [holder]. +/// +/// If the [holder] parameter is omitted, [ScopeConsumer] will automatically +/// perform a lookup using `ScopeProvider` and the current `BuildContext`. +/// +/// ```dart +/// ScopeConsumer( +/// listener: (context, scope) { +/// // do stuff here based on the scope +/// }, +/// builder: (context, scope) { +/// // return widget here based on the scope +/// } +/// ) +/// ``` +/// +/// Only specify the [holder] if you wish to provide a [holder] that is otherwise +/// not accessible via [ScopeProvider] and the current [BuildContext]. +/// +/// ```dart +/// ScopeConsumer( +/// holder: holder, +/// listener: (context, scope) { +/// // do stuff here based on the scope +/// }, +/// builder: (context, scope) { +/// // return widget here based on the scope +/// } +/// ) +/// ``` +class ScopeConsumer extends StatefulWidget { + final ScopeWidgetBuilder builder; + final ScopeWidgetListener listener; + final ScopeStateHolder? holder; + + const ScopeConsumer({ + required this.builder, + required this.listener, + this.holder, + super.key, + }); + + /// This is a factory that provides a non-null scope + /// in the builder and uses a [placeholder] if the scope is null. + factory ScopeConsumer.withPlaceholder({ + required NonNullableScopeWidgetBuilder builder, + required ScopeWidgetListener listener, + required Widget placeholder, + ScopeStateHolder? holder, + }) => + ScopeConsumer( + builder: (context, scope) { + if (scope == null) { + return placeholder; + } + return builder(context, scope); + }, + listener: listener, + holder: holder, + ); + + @override + State> createState() => _ScopeConsumerState(); +} + +class _ScopeConsumerState extends State> { + late ScopeStateHolder _holder; + + @override + void initState() { + super.initState(); + _holder = widget.holder ?? + ScopeProvider.scopeHolderOf( + context, + listen: false, + ); + } + + @override + void didUpdateWidget(covariant ScopeConsumer oldWidget) { + super.didUpdateWidget(oldWidget); + final oldHolder = + oldWidget.holder ?? ScopeProvider.scopeHolderOf(context, listen: false); + final currentHolder = widget.holder ?? oldHolder; + if (oldHolder != currentHolder) { + _holder = currentHolder; + } + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + final holder = widget.holder ?? ScopeProvider.scopeHolderOf(context); + if (holder != _holder) { + _holder = holder; + } + } + + @override + Widget build(BuildContext context) => ScopeListener( + holder: _holder, + listener: widget.listener, + child: ScopeBuilder( + holder: _holder, + builder: widget.builder, + ), + ); +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/lib/src/scope_listener.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/lib/src/scope_listener.dart new file mode 100644 index 0000000..d7f335e --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/lib/src/scope_listener.dart @@ -0,0 +1,108 @@ +import 'package:flutter/widgets.dart'; +import 'package:yx_scope/yx_scope.dart'; + +import 'scope_provider.dart'; +import 'scope_widget_listener.dart'; + +/// Takes a [ScopeListener] and an optional [holder] and invokes +/// the [listener] in response to `scope` changes in the [holder]. +/// It should be used for functionality that needs to occur only in response to +/// a `scope` change such as navigation, showing a `SnackBar`, showing +/// a `Dialog`, etc... +/// +/// If the [holder] is omited,[ScopeListener] will automatically +/// perform a lookup using [ScopeProvider] and the current [BuildContext]. +/// ```dart +/// ScopeListener( +/// listener: (context, scope) { +/// // do stuff here based on the scope +/// }, +/// cild: Container(), +/// ) +/// ``` +/// +/// Only specify the [holder] if you wish to provide a [holder] that is otherwise +/// not accessible via [ScopeProvider] and the current [BuildContext]. +/// +/// ```dart +/// ScopeListener( +/// holder: holder, +/// listener: (context, scope) { +/// // do stuff here based on the scope +/// }, +/// child: Container(), +/// ) +/// ``` +class ScopeListener extends StatefulWidget { + final ScopeWidgetListener listener; + final ScopeStateHolder? holder; + final Widget child; + + const ScopeListener({ + required this.listener, + required this.child, + this.holder, + super.key, + }); + + @override + State> createState() => ScopeListenerState(); +} + +class ScopeListenerState extends State> { + ScopeStateHolder? _holder; + RemoveStateListener? _removeStateListener; + + ScopeStateHolder get _actualHolder => + widget.holder ?? ScopeProvider.scopeHolderOf(context); + + @override + void dispose() { + _unsubscribe(); + super.dispose(); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + + _handleHolderChanged(); + } + + @override + void didUpdateWidget(covariant ScopeListener oldWidget) { + super.didUpdateWidget(oldWidget); + _handleHolderChanged(); + } + + void _handleHolderChanged() { + final previous = _holder; + final current = _actualHolder; + + if (previous == current) { + return; + } + + _holder = current; + _unsubscribe(); + _subscribe(current); + } + + void _subscribe(ScopeStateHolder holder) { + if (mounted) { + _removeStateListener = holder.listen((scope) { + if (mounted) { + widget.listener(context, scope); + } + }); + } + } + + void _unsubscribe() { + _removeStateListener?.call(); + _removeStateListener = null; + } + + @override + Widget build(BuildContext context) => widget.child; +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/lib/src/scope_provider.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/lib/src/scope_provider.dart new file mode 100644 index 0000000..4af449d --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/lib/src/scope_provider.dart @@ -0,0 +1,89 @@ +import 'package:flutter/widgets.dart'; +import 'package:yx_scope/yx_scope.dart'; + +import 'core/provider.dart'; +import 'core/scope_error.dart'; +import 'scope_builder.dart'; + +/// [ScopeProvider] is an [InheritedWidget] that passes your scope +/// down to any widget in the subtree. +/// +/// If you provide some scope container, you can get a nullable scope or +/// ScopeStateHolder of this scope container. +/// +/// ``` dart +/// final SomeScopeContainer? scope = ScopeProvider.of(context); +/// final ScopeStateHolder holder = ScopeProvider.scopeHolderOf(context); +/// ``` +class ScopeProvider extends StatelessWidget { + final ScopeStateHolder holder; + final Widget child; + + const ScopeProvider({ + required this.holder, + required this.child, + super.key, + }); + + /// This method provide a nullable cope, if there is a ScopeProvider widget higher up the tree + /// + /// If you want to use this method with the flag [listen] = true see more about the method + /// [BuildContext.dependOnInheritedWidgetOfExactType] to learn when to use this method. + /// + /// And if you want to use this method with the flag [listen] = false see more about the method + /// [BuildContext.getElementForInheritedWidgetOfExactType]. + static T? of(BuildContext context, {bool listen = true}) { + try { + return Provider.of(context, listen: listen); + } on NotFoundProviderException catch (_) { + throw FlutterScopeError(''' + ScopeProvider.of() called with a context that does not contain a $T. + No ancestor could be found starting from the context that was passed to ScopeProvider.of<$T>(). + + This can happen if the context you used comes from a widget above the ScopeProvider. + + The context used was: $context + '''); + } + } + + /// This method provide ScopeStateHolder<[T]>, if there is a ScopeProvider widget higher up the tree + /// + /// You can get ScopeStateHolder and use methods of it. But if you just want listen a scope, it's better + /// use [ScopeProvider.of] method with flag [listen]=true. + /// + /// If you want to use this method with the flag [listen] = true see more about the method + /// [BuildContext.dependOnInheritedWidgetOfExactType] to learn when to use this method. + /// + /// And if you want to use this method with the flag [listen] = false see more about the method + /// [BuildContext.getElementForInheritedWidgetOfExactType]. + static ScopeStateHolder scopeHolderOf( + BuildContext context, { + bool listen = true, + }) { + try { + return Provider.of>(context, listen: listen); + } on NotFoundProviderException catch (_) { + throw FlutterScopeError(''' + ScopeProvider.scopeHolderOf() called with a context that does not contain a $T. + No ancestor could be found starting from the context that was passed to ScopeProvider.scopeHolderOf<$T>(). + + This can happen if the context you used comes from a widget above the ScopeProvider. + + The context used was: $context + '''); + } + } + + @override + Widget build(BuildContext context) => Provider>( + data: holder, + child: ScopeBuilder( + holder: holder, + builder: (context, scope) => Provider( + data: scope, + child: child, + ), + ), + ); +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/lib/src/scope_widget_builder.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/lib/src/scope_widget_builder.dart new file mode 100644 index 0000000..824c664 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/lib/src/scope_widget_builder.dart @@ -0,0 +1,8 @@ +import 'package:flutter/widgets.dart'; + +typedef ScopeWidgetBuilder = Widget Function(BuildContext context, T? scope); + +typedef NonNullableScopeWidgetBuilder = Widget Function( + BuildContext context, + T scope, +); diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/lib/src/scope_widget_listener.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/lib/src/scope_widget_listener.dart new file mode 100644 index 0000000..ab5999b --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/lib/src/scope_widget_listener.dart @@ -0,0 +1,3 @@ +import 'package:flutter/widgets.dart'; + +typedef ScopeWidgetListener = void Function(BuildContext context, T? scope); diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/lib/yx_scope_flutter.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/lib/yx_scope_flutter.dart new file mode 100644 index 0000000..1b3b4c1 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/lib/yx_scope_flutter.dart @@ -0,0 +1,8 @@ +library yx_scope_flutter; + +export 'src/scope_builder.dart'; +export 'src/scope_consumer.dart'; +export 'src/scope_listener.dart'; +export 'src/scope_provider.dart'; +export 'src/scope_widget_builder.dart'; +export 'src/core/scope_error.dart'; diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/pubspec.yaml b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/pubspec.yaml new file mode 100644 index 0000000..680996f --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/pubspec.yaml @@ -0,0 +1,24 @@ +name: yx_scope_flutter +description: An adapter package that provides yx_scope widgets for Flutter UI integration +version: 1.1.3 +repository: https://github.com/yandex/city-services-pub/tree/main/yx_scope/packages/yx_scope_flutter +issue_tracker: https://github.com/yandex/city-services-pub/issues +topics: + - di + - scope + - dependency-injection + - dependency-management + +environment: + sdk: '>=2.19.0 <4.0.0' + flutter: ">=1.17.0" + +dependencies: + yx_scope: ^1.1.1 + flutter: + sdk: flutter + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^2.0.0 diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/pubspec_overrides.yaml b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/pubspec_overrides.yaml new file mode 100644 index 0000000..d249dfc --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/pubspec_overrides.yaml @@ -0,0 +1,3 @@ +dependency_overrides: + yx_scope: + path: ../yx_scope diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/test/scope_builder_test.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/test/scope_builder_test.dart new file mode 100644 index 0000000..0dfddf8 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/test/scope_builder_test.dart @@ -0,0 +1,189 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:yx_scope/yx_scope.dart' as yx_scope; +import 'package:yx_scope_flutter/yx_scope_flutter.dart'; + +import 'test_utils.dart'; + +class AppScopeContainer extends yx_scope.ScopeContainer { + late final counterProviderDep = dep(() => CounterProvider(0)); +} + +class AppScopeHolder extends yx_scope.ScopeHolder { + @override + AppScopeContainer createContainer() => AppScopeContainer(); +} + +class ScopeBuilderApp extends StatefulWidget { + final bool forceOneApp; + const ScopeBuilderApp({ + super.key, + this.forceOneApp = false, + }); + + @override + State createState() => _AppState(); +} + +class _AppState extends State { + bool showSecondApp = false; + final _appScopeHolder = AppScopeHolder(); + final _secondAppScopeHolder = AppScopeHolder(); + + @override + void initState() { + super.initState(); + _appScopeHolder.create(); + _secondAppScopeHolder.create(); + } + + @override + void dispose() { + _secondAppScopeHolder.drop(); + _appScopeHolder.drop(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return widget.forceOneApp + ? ScopeBuilderTestWidget( + secondAppScopeHolder: _secondAppScopeHolder, + forceOneApp: widget.forceOneApp, + ) + : ScopeProvider( + holder: _appScopeHolder, + child: ScopeBuilderTestWidget( + secondAppScopeHolder: _secondAppScopeHolder, + ), + ); + } +} + +class ScopeBuilderTestWidget extends StatefulWidget { + final AppScopeHolder secondAppScopeHolder; + final bool forceOneApp; + + const ScopeBuilderTestWidget({ + super.key, + required this.secondAppScopeHolder, + this.forceOneApp = false, + }); + + @override + State createState() => _ScopeBuilderTestWidgetState(); +} + +class _ScopeBuilderTestWidgetState extends State { + bool showSecondApp = false; + + @override + Widget build(BuildContext context) { + return ScopeBuilder.withPlaceholder( + // Replace ScopeHolder that ScopeBuilder will take from + // ScopeProvider as InheritedWidget to another ScopeHolder + holder: widget.forceOneApp || showSecondApp + ? widget.secondAppScopeHolder + : null, + builder: (context, appScope) { + return MaterialApp( + home: Scaffold( + body: Center( + child: Column( + children: [ + Text(appScope.counterProviderDep.get.count.toString()), + ElevatedButton( + onPressed: () { + // Add 1 to appScope that is currently stored in + // ScopeBuilder's ScopeHolder and change ScopeBuilder's + // ScopeHolder to another one + // Therefore value on the screen should increase by 1 + // every two taps + setState(() { + appScope.counterProviderDep.get.count += 1; + showSecondApp = !showSecondApp; + }); + }, + child: const SizedBox(), + ), + ], + ), + ), + ), + ); + }, + ); + } +} + +class ScopeListenerTestApp extends StatelessWidget { + final AppScopeHolder appScopeHolder; + final CounterProvider counter; + + const ScopeListenerTestApp({ + super.key, + required this.appScopeHolder, + required this.counter, + }); + + @override + Widget build(BuildContext context) { + return ScopeListener( + holder: appScopeHolder, + listener: (BuildContext context, AppScopeContainer? scope) { + counter.count++; + }, + child: const SizedBox(), + ); + } +} + +void main() { + testWidgets( + 'Don\'t throw exception when change ScopeBuilder\'s holder using setState', + (WidgetTester tester) async { + await tester.pumpWidget(const ScopeBuilderApp()); + await tester.tap(find.byType(ElevatedButton)); + await tester.pump(); + final dynamic exception = tester.takeException(); + expect(exception, isNull); + }); + + testWidgets('Check that ScopeBuilder subscribes to correct holder', + (WidgetTester tester) async { + await tester.pumpWidget(const ScopeBuilderApp()); + await tester.tap(find.byType(ElevatedButton)); + await tester.pump(); + expect((find.byType(Text).evaluate().single.widget as Text).data, '0'); + await tester.tap(find.byType(ElevatedButton)); + await tester.pump(); + expect((find.byType(Text).evaluate().single.widget as Text).data, '1'); + await tester.tap(find.byType(ElevatedButton)); + await tester.pump(); + expect((find.byType(Text).evaluate().single.widget as Text).data, '1'); + }); + + testWidgets( + 'Invoke ScopeListener\'s listen method only when scope state changes', + (WidgetTester tester) async { + final appScopeHolder = AppScopeHolder(); + final counter = CounterProvider(0); + await tester.pumpWidget(ScopeListenerTestApp( + appScopeHolder: appScopeHolder, + counter: counter, + )); + expect(counter.count, 0); + appScopeHolder.create(); + expect(counter.count, 1); + }); + + testWidgets( + 'ScopeBuilder should not throw exception when there is no ScopeProvider in context but it is passed as argument', + (WidgetTester tester) async { + await tester.pumpWidget(const ScopeBuilderApp(forceOneApp: true)); + await tester.tap(find.byType(ElevatedButton)); + await tester.pump(); + final dynamic exception = tester.takeException(); + expect(exception, isNull); + }); +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/test/scope_listener_test.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/test/scope_listener_test.dart new file mode 100644 index 0000000..a927bce --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/test/scope_listener_test.dart @@ -0,0 +1,84 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:yx_scope/yx_scope.dart'; +import 'package:yx_scope_flutter/yx_scope_flutter.dart' as yx_flutter; + +import 'test_utils.dart'; + +class TestScopeStateHolder extends ScopeHolder { + TestScopeStateHolder(); + + @override + TestScopeContainer createContainer() => TestScopeContainer(); +} + +class TestScopeContainer extends ScopeContainer {} + +class TestListenerApp extends StatelessWidget { + final ScopeStateHolder holder; + final CounterProvider listenerCounter; + final Widget child; + + const TestListenerApp({ + super.key, + required this.holder, + required this.listenerCounter, + required this.child, + }); + + @override + Widget build(BuildContext context) => yx_flutter.ScopeProvider( + holder: holder, + child: MaterialApp( + home: yx_flutter.ScopeListener( + listener: (context, scope) { + listenerCounter.count++; + }, + child: child, + ), + ), + ); +} + +void main() { + testWidgets('Listener is not called after disposal', (tester) async { + final holder = TestScopeStateHolder(); + final counter = CounterProvider(0); + + await tester.pumpWidget(TestListenerApp( + holder: holder, + listenerCounter: counter, + child: const SizedBox.shrink(), + )); + + await tester.pumpWidget(const SizedBox.shrink()); // Dispose widget tree + await holder.create(); + + expect(counter.count, 0); + }); + + testWidgets('Listener handles holder change correctly', (tester) async { + final holder1 = TestScopeStateHolder(); + final holder2 = TestScopeStateHolder(); + final counter = CounterProvider(0); + + await tester.pumpWidget(TestListenerApp( + holder: holder1, + listenerCounter: counter, + child: const SizedBox.shrink(), + )); + + await holder1.create(); + expect(counter.count, 1); + + // Change holder + await tester.pumpWidget(TestListenerApp( + holder: holder2, + listenerCounter: counter, + child: const SizedBox.shrink(), + )); + + await holder2.create(); + expect(counter.count, 2); + }); +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/test/scope_provider_test.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/test/scope_provider_test.dart new file mode 100644 index 0000000..6e37631 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/test/scope_provider_test.dart @@ -0,0 +1,207 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:yx_scope/yx_scope.dart'; +import 'package:yx_scope_flutter/yx_scope_flutter.dart'; + +import 'test_utils.dart'; + +class MyAppNoProvider extends MaterialApp { + const MyAppNoProvider({ + required Widget home, + Key? key, + }) : super(key: key, home: home); +} + +class MyApp extends StatelessWidget { + final Widget home; + final ScopeStateHolder holder; + const MyApp({super.key, required this.home, required this.holder}); + + @override + Widget build(BuildContext context) => ScopeProvider( + holder: holder, + child: MaterialApp(home: home), + ); +} + +class TestScopeStateHolder extends ScopeHolder { + TestScopeStateHolder(); + + @override + TestScopeContainer createContainer() => TestScopeContainer(); +} + +class TestScopeContainer extends ScopeContainer {} + +class ScopeWidget extends StatelessWidget { + final CounterProvider? buildsCounter; + const ScopeWidget({ + super.key, + this.buildsCounter, + }); + + @override + Widget build(BuildContext context) { + final _ = ScopeProvider.of(context); + buildsCounter?.count++; + return Container(); + } +} + +class NoScopeWidget extends StatelessWidget { + final CounterProvider? buildsCounter; + + const NoScopeWidget({ + super.key, + required this.buildsCounter, + }); + + @override + Widget build(BuildContext context) { + buildsCounter?.count++; + return Container(); + } +} + +abstract class SomeScope {} + +class SomeScopeContainer extends ScopeContainer implements SomeScope {} + +class SomeScopeHolder extends ScopeHolder { + @override + SomeScopeContainer createContainer() => SomeScopeContainer(); +} + +void main() { + testWidgets( + 'Throw exception if the context you used comes from a widget above the ScopeProvider', + (WidgetTester tester) async { + await tester.pumpWidget( + const MyAppNoProvider(home: ScopeWidget())); + final dynamic exception = tester.takeException(); + const expectedMessage = ''' + ScopeProvider.of() called with a context that does not contain a TestScopeContainer. + No ancestor could be found starting from the context that was passed to ScopeProvider.of(). + + This can happen if the context you used comes from a widget above the ScopeProvider. + + The context used was: ScopeWidget(dirty) +'''; + expect((exception as FlutterScopeError).message, expectedMessage); + }); + + testWidgets( + 'Don\'t throw exception if the context you used comes from a widget further down the ScopeProvider', + (WidgetTester tester) async { + await tester.pumpWidget(MyApp( + home: const ScopeWidget(), + holder: TestScopeStateHolder(), + )); + final dynamic exception = tester.takeException(); + expect(exception, isNull); + }); + + testWidgets( + 'Don\'t throw exception if you use an interface in the ScopeProvider.of() method and inject the same interface in a ScopeProvider', + (WidgetTester tester) async { + await tester.pumpWidget(MyApp( + home: const ScopeWidget(), + holder: SomeScopeHolder(), + )); + final dynamic exception = tester.takeException(); + expect(exception, isNull); + }); + + testWidgets( + 'Don\'t throw exception if you use a SomeScopeContainer that implemetns SomeScope in ScopeProvider.of and inject the same class into the ScopeProvider', + (WidgetTester tester) async { + await tester.pumpWidget(MyApp( + home: const ScopeWidget(), + holder: SomeScopeHolder(), + )); + final dynamic exception = tester.takeException(); + expect(exception, isNull); + }); + + testWidgets( + 'Don\'t throw exception if you dont explicitly write the interface SomeScope into the type for ScopeProvider and use SomeScopeContainer that implements SomeScopeContainer', + (WidgetTester tester) async { + await tester.pumpWidget(MyApp( + home: const ScopeWidget(), + holder: SomeScopeHolder(), + )); + final dynamic exception = tester.takeException(); + expect(exception, isNull); + }); + + testWidgets( + 'Throw exception if you use a SomeScope interface in ScopeProvider.of and inject SomeScopeContainer that implements SomeScope in ScopeProvider', + (WidgetTester tester) async { + await tester.pumpWidget(MyApp( + home: const ScopeWidget(), + holder: SomeScopeHolder(), + )); + final dynamic exception = tester.takeException(); + const expectedMessage = ''' + ScopeProvider.of() called with a context that does not contain a SomeScope. + No ancestor could be found starting from the context that was passed to ScopeProvider.of(). + + This can happen if the context you used comes from a widget above the ScopeProvider. + + The context used was: ScopeWidget(dirty) +'''; + expect((exception as FlutterScopeError).message, expectedMessage); + }); + + testWidgets( + 'Throw exception if you use SomeScopeContainer that implements SomeScope in ScopeProvider.of() method and inject SomeScope interface in ScopeProvider ', + (WidgetTester tester) async { + await tester.pumpWidget(MyApp( + home: const ScopeWidget(), + holder: SomeScopeHolder(), + )); + final dynamic exception = tester.takeException(); + const expectedMessage = ''' + ScopeProvider.of() called with a context that does not contain a SomeScopeContainer. + No ancestor could be found starting from the context that was passed to ScopeProvider.of(). + + This can happen if the context you used comes from a widget above the ScopeProvider. + + The context used was: ScopeWidget(dirty) +'''; + expect((exception as FlutterScopeError).message, expectedMessage); + }); + + testWidgets( + 'Scope updates reflect in subtree rebuilds only for children which have been subscribed to Scope via ScopeProvider.of', + (WidgetTester tester) async { + final holder = SomeScopeHolder(); + final scopeBuildsCounter = CounterProvider(0); + final noScopeBuildsCounter = CounterProvider(0); + await tester.pumpWidget(MyApp( + holder: holder, + home: Stack( + children: [ + ScopeWidget(buildsCounter: scopeBuildsCounter), + NoScopeWidget(buildsCounter: noScopeBuildsCounter), + ], + ), + )); + + await tester.pumpAndSettle(); + expect(scopeBuildsCounter.count, 1); + expect(noScopeBuildsCounter.count, 1); + + await holder.create(); + await tester.pumpAndSettle(); + + expect(scopeBuildsCounter.count, 2); + expect(noScopeBuildsCounter.count, 1); + + await holder.drop(); + await tester.pumpAndSettle(); + + expect(scopeBuildsCounter.count, 3); + expect(noScopeBuildsCounter.count, 1); + }); +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/test/test_utils.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/test/test_utils.dart new file mode 100644 index 0000000..679e547 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_flutter/test/test_utils.dart @@ -0,0 +1,5 @@ +class CounterProvider { + int count; + + CounterProvider(this.count); +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/.gitignore b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/.gitignore new file mode 100644 index 0000000..b4de1dc --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/.gitignore @@ -0,0 +1,32 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +.packages +build/ + +.arcignore diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/.metadata b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/.metadata new file mode 100644 index 0000000..eea17bc --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "2f708eb8396e362e280fac22cf171c2cb467343c" + channel: "stable" + +project_type: package diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/CHANGELOG.md b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/CHANGELOG.md new file mode 100644 index 0000000..3dc804e --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/CHANGELOG.md @@ -0,0 +1,36 @@ +## 0.1.4 - 2025.07.08 + +* Documentation links fixed + +## 0.1.3 - 2025.07.07 + +* Fix: dep_cycle rule support for ScopeModule +* avoid_async_dep_child_scope lint rule added +* Linter CI added to project +* Analyzer version upgrade to v6.4.1 +* Documentation fixes and updates + +## 0.1.2 - 2024.12.18 + +* Two new manual lint rules: `avoid_child_scope_in_initialize_queue` + and `avoid_conditions_in_initialize_queue` +* Updated repository link +* Library topics added + +## 0.1.1 - 2024.10.23 + +* Minor fixes in links + +## 0.1.0 + +* Ready to be open-source (but not finished yet) + +## 0.0.1 + +* Added lints: + +- consider_dep_suffix +- final_dep +- dep_cycle +- pass_async_lifecycle_in_initialize_queue +- use_async_dep_for_async_lifecycle diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/LICENSE b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/LICENSE new file mode 100644 index 0000000..bd7b143 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2024 YANDEX LLC + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/README.md b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/README.md new file mode 100644 index 0000000..6b1e0f3 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/README.md @@ -0,0 +1,154 @@ +# yx_scope_linter + +## Table of contents + +- [Installing yx\_scoped\_linter](#installing-yx_scope_linter) +- [Enabling/disabling lints](#enablingdisabling-lints) + - [Disable one specific rule](#disable-one-specific-rule) + - [Disable all lints by default](#disable-all-lints-by-default) +- [All the lints](#all-the-lints) + - [consider_dep_suffix](#consider_dep_suffix) + - [dep_cycle](#dep_cycle) + - [final_dep](#final_dep) + - [pass_async_lifecycle_in_initialize_queue](#pass_async_lifecycle_in_initialize_queue) + - [use_async_dep_for_async_lifecycle](#use_async_dep_for_async_lifecycle) + +## Installing yx_scope_linter + +yx_scope_linter is implemented using [custom_lint]. As such, it uses custom_lint's installation +logic. +Long story short: + +- Add both yx_scope_linter and custom_lint to your `pubspec.yaml`: + ```yaml + dev_dependencies: + custom_lint: + yx_scope_linter: + ``` +- Enable `custom_lint`'s plugin in your `analysis_options.yaml`: + + ```yaml + analyzer: + plugins: + - custom_lint + ``` + +## Enabling/disabling lints. + +By default when installing yx_scope_linter, most of the lints will be enabled. +To change this, you have a few options. + +### Disable one specific rule + +You may dislike one of the various lint rules offered by yx_scope_linter. +In that event, you can explicitly disable this lint rule for your project +by modifying the `analysis_options.yaml` + +```yaml +analyzer: + plugins: + - custom_lint + +custom_lint: + rules: + # Explicitly disable one lint rule + - consider_dep_suffix: false +``` + +### Disable all lints by default + +Instead of having all lints on by default and manually disabling lints of your choice, +you can switch to the opposite logic: +Have lints off by default, and manually enable lints. + +This can be done in your `analysis_options.yaml` with the following: + +```yaml +analyzer: + plugins: + - custom_lint + +custom_lint: + # Forcibly disable lint rules by default + enable_all_lint_rules: false + rules: + # You can now enable one specific rule in the "rules" list + - consider_dep_suffix +``` + +## All the lints + +### consider_dep_suffix + +For dependencies in the Scope Container, use a name with the suffix "Dep". + +**Good**. + +```dart + +late final myDep = dep(() => MyDep()); +``` + +**Bad**. + +```dart + +late final justMyString = dep(() => SomeDep()); +``` + +### dep_cycle + +A cyclical dependency has been identified. It is necessary to eliminate it. + +**Bad**. + +```dart +// The cycle is detected: my1Dep <- my3Dep <- my2Dep <- my1Dep (dep_cycle) +late final Dep my1Dep = dep(() => MyDep1(my3Dep)); +late final Dep my2Dep = dep(() => MyDep2(my1Dep)); +late final Dep my3Dep = dep(() => MyDep3(my2Dep)); +``` + +### final_dep + +A dep field must be `late final` + +**Good**. + +```dart + +late final myDep = dep(() => MyDep()); +``` + +**Bad**. + +```dart + +final myDep = dep(() => SomeDep()); + +var myDep = dep(() => SomeDep()); + +late var myDep = dep(() => SomeDep()); +``` + +### pass_async_lifecycle_in_initialize_queue + +A dependencies that is asyncDep or rawAsyncDep must be passed to initializeQueue. Otherwise +init/dispose methods will not be called. + +### use_async_dep_for_async_lifecycle + +Dependency implements AsyncLifecycle interface, must be use asyncDep or rawAsyncDep. Otherwise +init/dispose methods will not be called. + +### avoid_child_scope_in_initialize_queue + +A child scope should not be an `asyncDep` inside a parent scope. If this happens, it means the child +and parent scopes have the same lifecycle. In this case, the child scope is likely unnecessary. It +is just a subset of the parent scope’s dependencies. Use `ScopeModule` to separate these +dependencies logically. They will still belong to the same scope and share its lifecycle. + +## Manual lints + +Not all of the lint rules are implemented so far. So there are all not implemented +ones: [manual linter](doc/manual_linter.md). diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/analysis_options.yaml b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/analysis_options.yaml new file mode 100644 index 0000000..572dd23 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/analysis_options.yaml @@ -0,0 +1 @@ +include: package:lints/recommended.yaml diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/doc/manual_linter.md b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/doc/manual_linter.md new file mode 100644 index 0000000..eae427d --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/doc/manual_linter.md @@ -0,0 +1,72 @@ +### avoid_call_dep_method_inside_functions + +Do not invoke the `dep` and `asyncDep` methods from within functions. Only assign them +to `late final` fields. + +### avoid_direct_scope_child + +If a `Dep` within a `ScopeContainer` is responsible for a child scope, it should be a holder (a +subclass of `CoreScopeHolder`), not a `ScopeContainer`. + +### consider_module_suffix + +Do not use the `Dep` suffix for `ScopeModule` fields. Use the `Module` suffix: `entityNameModule`. + +### avoid_sync_init_dispose + +Entities that require initialization are often initialized asynchronously. Similarly, disposal often +requires asynchronous execution (e.g., `subscription.cancel()`). Using synchronous `init`/`dispose` +methods can lead to asynchronous functions being called without `await`, potentially causing +non-deterministic initialization or disposal order. To avoid this, even synchronous initialization +should be handled in asynchronous `init`/`dispose` methods. + +### avoid_passing_deps_in_constructor + +Do not pass `Dep` into the constructor of other entities. `Dep` should only be used within +a `ScopeContainer`. + +### order_initialize_queue_first + +Declare `initializeQueue` as the first member in `ScopeContainer`. This helps to quickly identify +asynchronous dependencies and their execution order. + +### wrong_scope_container_fields_order + +Declare fields within a `ScopeContainer` in the following order (if applicable): + +1. `initializeQueue` getter (if necessary) +2. All `ScopeHolder` fields for child scopes +3. All `ScopeModule` fields for the current scope +4. All `Dep` fields + +The order of private/public fields is not regulated. + +### avoid_cache_dep_outscope + +Do not assign `Dep` to fields or global variables outside the scope. + +### avoid_scope_without_holder + +Do not instantiate a scope manually outside of a `ScopeHolder`. + +### avoid_creating_child_holder_outside_parent_scope + +Do not instantiate a child scope outside the parent scope. + +### create_container_always_protected + +The `createContainer` method should always be `protected`, even in subclasses. This ensures that no +one calls `createContainer` directly, but instead uses the `create` method. + +### container_or_dep_in_module_constructor + +When instantiating a `ScopeModule`, only pass a `ScopeContainer` or a `Dep` to its constructor, not +instances of classes. Otherwise, there is a risk of breaking the scope's lifecycle. For example, +passing `ScopeModule(someDep.get)` would cause `someDep` to be instantiated immediately upon module +creation, rather than lazily when the dependency is first accessed. + +### avoid_conditions_in_initialize_queue + +All asynchronous dependencies in a scope must be initialized. This ensures that accessing any +dependency always works predictably. If a dependency is declared, it must be ready to use. Any +conditions for initialization should be placed inside the init method. \ No newline at end of file diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/example/.gitignore b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/example/.gitignore new file mode 100644 index 0000000..20857a4 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/example/.gitignore @@ -0,0 +1,31 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +.dart_tool/ +.packages +build/ +/pubspec.lock + +.arcignore diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/example/.metadata b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/example/.metadata new file mode 100644 index 0000000..eea17bc --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/example/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "2f708eb8396e362e280fac22cf171c2cb467343c" + channel: "stable" + +project_type: package diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/example/CHANGELOG.md b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/example/CHANGELOG.md new file mode 100644 index 0000000..41cc7d8 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/example/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1 + +* TODO: Describe initial release. diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/example/LICENSE b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/example/LICENSE new file mode 100644 index 0000000..ba75c69 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/example/LICENSE @@ -0,0 +1 @@ +TODO: Add your license here. diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/example/README.md b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/example/README.md new file mode 100644 index 0000000..02fe8ec --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/example/README.md @@ -0,0 +1,39 @@ + + +TODO: Put a short description of the package here that helps potential users +know whether this package might be useful for them. + +## Features + +TODO: List what your package can do. Maybe include images, gifs, or videos. + +## Getting started + +TODO: List prerequisites and provide or point to information on how to +start using the package. + +## Usage + +TODO: Include short and useful examples for package users. Add longer examples +to `/example` folder. + +```dart +const like = 'sample'; +``` + +## Additional information + +TODO: Tell users more about the package: where to find more information, how to +contribute to the package, how to file issues, what response they can expect +from the package authors, and more. diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/example/analysis_options.yaml b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/example/analysis_options.yaml new file mode 100644 index 0000000..019f449 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/example/analysis_options.yaml @@ -0,0 +1,3 @@ +analyzer: + plugins: + - custom_lint diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/example/lib/avoid_async_dep_child_scope.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/example/lib/avoid_async_dep_child_scope.dart new file mode 100644 index 0000000..c9017bd --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/example/lib/avoid_async_dep_child_scope.dart @@ -0,0 +1,54 @@ +import 'package:yx_scope/yx_scope.dart'; + +class _ParentScopeContainer extends ScopeContainer { + @override + List> get initializeQueue => [ + {_childScopeHolderRawAsyncDep, _childScopeHolderAsyncDep} + ]; + + // expect_lint: avoid_async_dep_child_scope + late final _childScopeHolderRawAsyncDep = rawAsyncDep( + () => _ChildScopeHolder(this), + init: (dep) async => await dep.create(), + dispose: (dep) async => await dep.drop(), + ); + + // expect_lint: avoid_async_dep_child_scope + late final _childScopeHolderAsyncDep = + asyncDep(() => _ChildAsyncLifecycleScopeContainer()); + + late final parentModule = ParentScopeModule(this); +} + +class ParentScopeModule extends ScopeModule<_ParentScopeContainer> { + ParentScopeModule(super.container); + + // expect_lint: avoid_async_dep_child_scope + late final childScopeHolderRawAsyncDep = rawAsyncDep( + () => _ChildScopeHolder(this.container), + init: (dep) async => await dep.create(), + dispose: (dep) async => await dep.drop(), + ); +} + +class _ChildScopeHolder + extends ChildScopeHolder<_ChildScopeContainer, _ParentScopeContainer> { + _ChildScopeHolder(super.parent); + + @override + _ChildScopeContainer createContainer(_ParentScopeContainer parent) => + _ChildScopeContainer(parent: parent); +} + +class _ChildScopeContainer extends ChildScopeContainer<_ParentScopeContainer> { + _ChildScopeContainer({required super.parent}); +} + +class _ChildAsyncLifecycleScopeContainer extends ScopeContainer + implements AsyncLifecycle { + @override + Future dispose() async {} + + @override + Future init() async {} +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/example/lib/consider_dep_suffix.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/example/lib/consider_dep_suffix.dart new file mode 100644 index 0000000..2c681cf --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/example/lib/consider_dep_suffix.dart @@ -0,0 +1,31 @@ +import 'package:yx_scope/yx_scope.dart'; + +class SomeScope extends ScopeContainer { + @override + List> get initializeQueue => [ + {justMyString}, + {rawAsync} + ]; + + late final my1Dep = dep(() => '1'); + + // expect_lint: consider_dep_suffix + late final myDep2 = dep(() => '2'); + + // expect_lint: consider_dep_suffix + late final justMyString = asyncDep(() => SomeAsyncDep()); + + // expect_lint: consider_dep_suffix + late final rawAsync = + rawAsyncDep(() => '4', init: (value) async {}, dispose: (value) async {}); +} + +class SomeAsyncDep implements AsyncLifecycle { + const SomeAsyncDep(); + + @override + Future init() async {} + + @override + Future dispose() async {} +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/example/lib/dep_cycle.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/example/lib/dep_cycle.dart new file mode 100644 index 0000000..3c61fd6 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/example/lib/dep_cycle.dart @@ -0,0 +1,137 @@ +import 'package:example/utils.dart'; +import 'package:yx_scope/yx_scope.dart'; + +class SomeScope extends BaseScopeContainer { + late final Dep some0Dep = + dep(() => SomeDep0(someScopeModule.some6Dep.get)); + + @override + List> get initializeQueue => [ + { + some2Dep, + someScopeModule.some4Dep, + anysome2Dep, + } + ]; + + // Some comment + late final Dep some0WithCommentDep = + dep(() => SomeDep0(someScopeModule.some6Dep.get)); + + // expect_lint: dep_cycle + late final Dep some1Dep = + dep(() => SomeDep1(some2: anysome2Dep.get)); + + // Dependencies with same substring part sould not break dep cycle linter. + late final some2Dep = asyncDep(() => SomeDep2(some3Dep.get)); + + // expect_lint: dep_cycle + late final anysome2Dep = asyncDep(() => SomeDep2(some3Dep.get)); + + // expect_lint: dep_cycle + late final Dep some3Dep = dep(() { + final dep = _createSome3Dep(); + return dep; + }); + + SomeDep3 _createSome3Dep() { + final some4 = someScopeModule.some4Dep.get; + return SomeDep3(some1Dep.get, some4: some4); + } + + // expect_lint: dep_cycle + late final someScopeModule = SomeScopeModule(this); + + SomeDep4 createSome4Dep() { + return SomeDep4(some1Dep.get, some3Dep.get); + } + + // expect_lint: dep_cycle + late final scopeModuleFromAnotherFile = UtilsScopeModule(this); + + // expect_lint: dep_cycle + late final some7Dep = + dep(() => SomeDep7(scopeModuleFromAnotherFile.someUtilsDep.get)); +} + +class SomeDep0 { + final SomeDep6 some6; + + SomeDep0(this.some6); +} + +class SomeDep1 { + final SomeDep2 some2; + + const SomeDep1({required this.some2}); +} + +class SomeDep2 implements AsyncLifecycle { + final SomeDep3 some3; + + const SomeDep2(this.some3); + + @override + Future init() async {} + + @override + Future dispose() async {} +} + +class SomeDep3 { + final SomeDep1 some1; + final SomeDep4 some4; + + const SomeDep3(this.some1, {required this.some4}); +} + +class SomeDep4 { + final SomeDep1 some1; + final SomeDep3 some3; + + const SomeDep4(this.some1, this.some3); + + void init() {} + + void dispose() {} +} + +class SomeDep5 { + final SomeDep6 some6; + + const SomeDep5(this.some6); +} + +class SomeDep6 { + final SomeDep5 some5; + + SomeDep6(this.some5); +} + +class SomeDep7 { + final SomeUtilsDep someUtils; + + SomeDep7(this.someUtils); +} + +class SomeScopeModule extends ScopeModule { + SomeScopeModule(super.container); + + late final some4Dep = rawAsyncDep( + () { + return container.createSome4Dep(); + }, + init: (dep) async => dep.init(), + dispose: (dep) async => dep.dispose(), + ); + + // expect_lint: dep_cycle + late final Dep some5Dep = dep(() => _createSome5Dep()); + + SomeDep5 _createSome5Dep() => SomeDep5(some6Dep.get); + + // expect_lint: dep_cycle + late final some6Dep = dep(_createSome6Dep); + + SomeDep6 _createSome6Dep() => SomeDep6(some5Dep.get); +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/example/lib/final_dep.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/example/lib/final_dep.dart new file mode 100644 index 0000000..b5506dd --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/example/lib/final_dep.dart @@ -0,0 +1,50 @@ +import 'package:yx_scope/yx_scope.dart'; + +class SomeScope extends BaseScopeContainer { + static const someConstant = 'some_constant'; + + @override + List> get initializeQueue => [ + {lateFinalAsyncDep} + ]; + + var someRandomField = 'random_field'; + + late final lateFinalDep = dep(() => '1'); + + // expect_lint: final_dep + late var finalDep = dep(() => '2'); + + late final lateFinalAsyncDep = asyncDep(() => SomeAsyncDep()); + + // expect_lint: final_dep, pass_async_lifecycle_in_initialize_queue + late var lateAsyncDep = asyncDep(() => SomeAsyncDep()); + + // expect_lint: final_dep, pass_async_lifecycle_in_initialize_queue + late AsyncDep explicitTypeLateAsyncDep = + asyncDep(() => SomeAsyncDep()); + + // expect_lint: pass_async_lifecycle_in_initialize_queue + late final AsyncDep explicitTypeLateFinalAsyncDep = + asyncDep(() => SomeAsyncDep()); + + // expect_lint: final_dep, pass_async_lifecycle_in_initialize_queue + late var lateRawAsyncDep = + rawAsyncDep(() => '3', init: (dep) async {}, dispose: (dep) async {}); + + // expect_lint: pass_async_lifecycle_in_initialize_queue + late final lateFinalRawAsyncDep = + rawAsyncDep(() => '4', init: (dep) async {}, dispose: (dep) async {}); + + void someMethod() {} +} + +class SomeAsyncDep implements AsyncLifecycle { + const SomeAsyncDep(); + + @override + Future init() async {} + + @override + Future dispose() async {} +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/example/lib/pass_async_lifecycle_in_initialize_queue.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/example/lib/pass_async_lifecycle_in_initialize_queue.dart new file mode 100644 index 0000000..a02da43 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/example/lib/pass_async_lifecycle_in_initialize_queue.dart @@ -0,0 +1,42 @@ +import 'package:yx_scope/yx_scope.dart'; + +class SomeScope extends BaseScopeContainer { + static void someStaticMethod() {} + + @override + List> get initializeQueue => [ + { + justMyString, + } + ]; + + void someMethod() {} + + void someGenericMethod() {} + + Future someAsyncMethod() async {} + + String get someGetter => ''; + + late final my1Dep = dep(() => '1'); + + // expect_lint: consider_dep_suffix + late final myDep2 = dep(() => '2'); + + // expect_lint: consider_dep_suffix + late final justMyString = asyncDep(() => SomeAsyncDep()); + + // expect_lint: consider_dep_suffix, pass_async_lifecycle_in_initialize_queue + late final rawAsync = + rawAsyncDep(() => '4', init: (value) async {}, dispose: (value) async {}); +} + +class SomeAsyncDep implements AsyncLifecycle { + const SomeAsyncDep(); + + @override + Future init() async {} + + @override + Future dispose() async {} +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/example/lib/use_async_dep_for_async_lifecycle.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/example/lib/use_async_dep_for_async_lifecycle.dart new file mode 100644 index 0000000..ea56bff --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/example/lib/use_async_dep_for_async_lifecycle.dart @@ -0,0 +1,18 @@ +import 'package:yx_scope/yx_scope.dart'; + +class SomeScope extends BaseScopeContainer { + late final syncDep = dep(() => '1'); + + // expect_lint: use_async_dep_for_async_lifecycle + late final shouldBeAsyncDep = dep(() => AsyncLifecycleButSyncDeclaration()); +} + +class AsyncLifecycleButSyncDeclaration implements AsyncLifecycle { + const AsyncLifecycleButSyncDeclaration(); + + @override + Future init() async {} + + @override + Future dispose() async {} +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/example/lib/utils.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/example/lib/utils.dart new file mode 100644 index 0000000..83d4f2e --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/example/lib/utils.dart @@ -0,0 +1,24 @@ +import 'package:example/dep_cycle.dart'; +import 'package:yx_scope/yx_scope.dart'; + +class UtilsScopeModule extends ScopeModule { + UtilsScopeModule(super.container); + + late final some7Dep = rawAsyncDep( + () { + return container.createSome4Dep(); + }, + init: (dep) async => dep.init(), + dispose: (dep) async => dep.dispose(), + ); + + late final Dep someUtilsDep = dep(() => _createSomeUtilsDep()); + + SomeUtilsDep _createSomeUtilsDep() => SomeUtilsDep(container.some7Dep.get); +} + +class SomeUtilsDep { + final SomeDep7 some7; + + SomeUtilsDep(this.some7); +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/example/pubspec.yaml b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/example/pubspec.yaml new file mode 100644 index 0000000..82c1eb3 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/example/pubspec.yaml @@ -0,0 +1,16 @@ +name: example +description: An example for yx_scope_linter rules +version: 1.0.0 +publish_to: 'none' + +environment: + sdk: '>=2.18.0 <4.0.0' + +dependencies: + yx_scope: ^1.0.0 + +dev_dependencies: + lints: ^2.0.0 + custom_lint: ^0.6.8 + yx_scope_linter: + path: .. diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/example/pubspec_overrides.yaml b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/example/pubspec_overrides.yaml new file mode 100644 index 0000000..fab25a1 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/example/pubspec_overrides.yaml @@ -0,0 +1,3 @@ +dependency_overrides: + yx_scope: + path: ../../yx_scope diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/lib/src/extensions.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/lib/src/extensions.dart new file mode 100644 index 0000000..e6d8c0e --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/lib/src/extensions.dart @@ -0,0 +1,13 @@ +import 'package:custom_lint_builder/custom_lint_builder.dart'; + +extension LintCodeCopyWith on LintCode { + LintCode copyWith({String? problemMessage, String? correctionMessage}) => + LintCode( + name: name, + problemMessage: problemMessage ?? this.problemMessage, + correctionMessage: correctionMessage ?? this.correctionMessage, + uniqueName: uniqueName, + url: url, + errorSeverity: errorSeverity, + ); +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/lib/src/lints/avoid_async_dep_child_scope.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/lib/src/lints/avoid_async_dep_child_scope.dart new file mode 100644 index 0000000..028d99d --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/lib/src/lints/avoid_async_dep_child_scope.dart @@ -0,0 +1,55 @@ +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/element/element.dart'; +import 'package:analyzer/dart/element/type.dart'; +import 'package:analyzer/error/error.dart' hide LintCode; +import 'package:analyzer/error/listener.dart'; +import 'package:custom_lint_builder/custom_lint_builder.dart'; +import 'package:yx_scope_linter/src/types.dart'; + +import '../yx_scope_lint_rule.dart'; + +class AvoidChildScopeInInitializeQueue extends YXScopeLintRule { + static const _code = LintCode( + name: 'avoid_async_dep_child_scope', + problemMessage: 'Child scope should not have the same lifecycle as its ' + 'parent, and therefore the child scope should be neither ' + 'an asyncDep nor a rawAsyncDep', + correctionMessage: 'If you need some dependencies with the same lifecycle ' + 'but grouped together, use ScopeModule instead', + errorSeverity: ErrorSeverity.WARNING, + ); + + const AvoidChildScopeInInitializeQueue() : super(code: _code); + + @override + void run( + CustomLintResolver resolver, + ErrorReporter reporter, + CustomLintContext context, + ) { + yxScopeRegistry(context).addScopeDeclarations((module) { + for (final dep in module.deps.values) { + final methodInvocation = dep.field.fields.childEntities + .whereType() + .expand((e) => e.childEntities.whereType()) + .first; + final depClass = (methodInvocation.staticType as InterfaceType) + .typeArguments + .map((e) => e.element) + .whereType() + .first; + final isScopeHolder = childScopeHolderValueType.isSuperOf(depClass); + final isScopeContainer = + scopeContainerValueType.isAssignableFrom(depClass); + final isAsyncLifecycle = asyncLifecycleType.isAssignableFrom(depClass); + if (dep.isAsync && + (isScopeHolder || (isScopeContainer && isAsyncLifecycle))) { + reporter.atToken( + dep.nameToken, + _code, + ); + } + } + }); + } +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/lib/src/lints/consider_dep_suffix.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/lib/src/lints/consider_dep_suffix.dart new file mode 100644 index 0000000..8ec1537 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/lib/src/lints/consider_dep_suffix.dart @@ -0,0 +1,77 @@ +import 'package:analyzer/error/error.dart' hide LintCode; +import 'package:analyzer/error/listener.dart'; +import 'package:custom_lint_builder/custom_lint_builder.dart'; +import 'package:yx_scope_linter/src/extensions.dart'; + +import '../models/dep.dart'; +import '../priority.dart'; +import '../yx_scope_lint_rule.dart'; + +const _suffix = 'Dep'; + +class ConsiderDepSuffix extends YXScopeLintRule { + static const _code = LintCode( + name: 'consider_dep_suffix', + problemMessage: 'Consider using suffix `$_suffix` for the name of your Dep', + correctionMessage: 'Add suffix `$_suffix` like this: `entityName$_suffix`', + errorSeverity: ErrorSeverity.INFO, + ); + + const ConsiderDepSuffix() : super(code: _code); + + @override + void run( + CustomLintResolver resolver, + ErrorReporter reporter, + CustomLintContext context, + ) { + yxScopeRegistry(context).addScopeDeclarations((module) { + void checkSuffix(BaseScopeDeclaration module) { + for (final dep in module.deps.values) { + if (dep.name.endsWith(_suffix)) { + continue; + } + + reporter.atToken( + dep.nameToken, + _code.copyWith( + correctionMessage: 'Change the name to `${dep.name}$_suffix`', + ), + ); + } + + for (final module in module.modules.values) { + checkSuffix(module); + } + } + + checkSuffix(module); + }); + } + + @override + List getFixes() => [ConsiderDepSuffixAssist()]; +} + +class ConsiderDepSuffixAssist extends DartFix { + @override + void run( + CustomLintResolver resolver, + ChangeReporter reporter, + CustomLintContext context, + AnalysisError analysisError, + List others, + ) { + final changeBuilder = reporter.createChangeBuilder( + message: analysisError.correctionMessage!, + priority: FixPriority.considerDepSuffix.value, + ); + + changeBuilder.addDartFileEdit((builder) { + builder.addSimpleInsertion( + analysisError.sourceRange.end, + _suffix, + ); + }); + } +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/lib/src/lints/dep_cycle.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/lib/src/lints/dep_cycle.dart new file mode 100644 index 0000000..5702737 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/lib/src/lints/dep_cycle.dart @@ -0,0 +1,81 @@ +import 'package:analyzer/error/error.dart' hide LintCode; +import 'package:analyzer/error/listener.dart'; +import 'package:custom_lint_builder/custom_lint_builder.dart'; +import 'package:yx_scope_linter/src/extensions.dart'; + +import '../models/dep.dart'; +import '../yx_scope_lint_rule.dart'; + +class DepCycle extends YXScopeLintRule { + static const _name = 'dep_cycle'; + static const _message = 'The cycle is detected'; + static const _code = LintCode( + name: _name, + problemMessage: _message, + errorSeverity: ErrorSeverity.ERROR, + ); + + const DepCycle() : super(code: _code); + + @override + void run( + CustomLintResolver resolver, + ErrorReporter reporter, + CustomLintContext context, + ) { + yxScopeRegistry(context).addScopeDeclarations((scope) { + final cycles = detectCycles(scope); + for (final cycle in cycles) { + final errorCode = _code.copyWith( + problemMessage: '$_message: ${cycle.map((e) => e).join(' <- ')}' + ' <- ${cycle.first}', + ); + + for (final dep in cycle) { + if (dep.parent == scope) { + reporter.atToken( + dep.nameToken, + errorCode, + ); + } else { + reporter.atToken( + (dep.parent as ModuleDeclaration).nameToken!, + errorCode, + ); + } + } + } + }); + } + + List> detectCycles(BaseScopeDeclaration module) { + final cycles = >[]; + final visited = {}; + final inStack = {}; + + void dfs(DepDeclaration entity, List currentCycle) { + visited.add(entity); + inStack.add(entity); + currentCycle.add(entity); + + for (final dependency in entity.deps.values) { + if (!visited.contains(dependency)) { + dfs(dependency, currentCycle); + } else if (inStack.contains(dependency)) { + cycles.add(currentCycle.sublist(currentCycle.indexOf(dependency))); + } + } + + inStack.remove(entity); + currentCycle.remove(entity); + } + + for (final entity in module.deps.values) { + if (!visited.contains(entity)) { + dfs(entity, []); + } + } + + return cycles; + } +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/lib/src/lints/final_dep.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/lib/src/lints/final_dep.dart new file mode 100644 index 0000000..52adea4 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/lib/src/lints/final_dep.dart @@ -0,0 +1,88 @@ +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/error/error.dart' hide LintCode; +import 'package:analyzer/error/listener.dart'; +import 'package:custom_lint_builder/custom_lint_builder.dart'; + +import '../models/dep.dart'; +import '../priority.dart'; +import '../yx_scope_lint_rule.dart'; + +class FinalDep extends YXScopeLintRule { + static const _code = LintCode( + name: 'final_dep', + problemMessage: 'A dep field must be `late final`', + correctionMessage: 'Make your dep field `final`', + errorSeverity: ErrorSeverity.WARNING, + ); + + const FinalDep() : super(code: _code); + + @override + void run( + CustomLintResolver resolver, + ErrorReporter reporter, + CustomLintContext context, + ) { + yxScopeRegistry(context).addScopeDeclarations((module) { + void checkFinal(BaseScopeDeclaration module) { + for (final dep in module.deps.values) { + final field = dep.field; + + if (!field.fields.isFinal) { + reporter.atToken( + dep.nameToken, + _code, + data: field.fields, + ); + } + } + + for (final module in module.modules.values) { + checkFinal(module); + } + } + + checkFinal(module); + }); + } + + @override + List getFixes() => [FinalDepFix()]; +} + +class FinalDepFix extends DartFix { + @override + void run( + CustomLintResolver resolver, + ChangeReporter reporter, + CustomLintContext context, + AnalysisError analysisError, + List others, + ) { + final changeBuilder = reporter.createChangeBuilder( + message: analysisError.correctionMessage!, + priority: FixPriority.finalDep.value, + ); + + final fields = analysisError.data as VariableDeclarationList; + + changeBuilder.addDartFileEdit((builder) { + final keyword = fields.keyword; + if (keyword != null) { + builder.addSimpleReplacement( + keyword.sourceRange, + 'final', + ); + } else { + builder.addSimpleInsertion(fields.lateKeyword!.end, ' final'); + } + + final type = fields.type; + if (type != null) { + builder.addDeletion(type.sourceRange); + } + + builder.format(fields.sourceRange); + }); + } +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/lib/src/lints/pass_async_lifecycle_in_initialize_queue.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/lib/src/lints/pass_async_lifecycle_in_initialize_queue.dart new file mode 100644 index 0000000..b779f6a --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/lib/src/lints/pass_async_lifecycle_in_initialize_queue.dart @@ -0,0 +1,44 @@ +import 'package:analyzer/error/error.dart' hide LintCode; +import 'package:analyzer/error/listener.dart'; +import 'package:custom_lint_builder/custom_lint_builder.dart'; +import 'package:yx_scope_linter/src/types.dart'; + +import '../yx_scope_lint_rule.dart'; + +class PassAsyncLifecycleInInitializeQueue extends YXScopeLintRule { + static const _code = LintCode( + name: 'pass_async_lifecycle_in_initialize_queue', + problemMessage: + 'asyncDep (or rawAsyncDep) must be passed to initializeQueue. ' + 'Otherwise init/dispose methods will not be called.', + correctionMessage: 'Override method initializeQueue in the current scope' + ' and pass the Dep there', + errorSeverity: ErrorSeverity.WARNING, + ); + + const PassAsyncLifecycleInInitializeQueue() : super(code: _code); + + @override + void run( + CustomLintResolver resolver, + ErrorReporter reporter, + CustomLintContext context, + ) { + yxScopeRegistry(context).addScopeDeclarations((scope) { + if (scopeModuleType.isAssignableFromType(scope.type)) { + return; + } + for (final dep in scope.deps.values) { + if (dep.isSync) { + continue; + } + if (!scope.initializeQueue.expand((element) => element).contains(dep)) { + reporter.atToken( + dep.nameToken, + _code, + ); + } + } + }); + } +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/lib/src/lints/use_async_dep_for_async_lifecycle.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/lib/src/lints/use_async_dep_for_async_lifecycle.dart new file mode 100644 index 0000000..3610bb8 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/lib/src/lints/use_async_dep_for_async_lifecycle.dart @@ -0,0 +1,94 @@ +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/element/element.dart'; +import 'package:analyzer/dart/element/type.dart'; +import 'package:analyzer/error/error.dart' hide LintCode; +import 'package:analyzer/error/listener.dart'; +import 'package:custom_lint_builder/custom_lint_builder.dart'; + +import '../models/dep.dart'; +import '../priority.dart'; +import '../types.dart'; +import '../yx_scope_lint_rule.dart'; + +const _asyncDepKeyword = 'asyncDep'; + +class UseAsyncDepForAsyncLifecycle extends YXScopeLintRule { + static const _code = LintCode( + name: 'use_async_dep_for_async_lifecycle', + problemMessage: + 'Dependency implements AsyncLifecycle interface, but uses `dep` declaration. ' + 'In this case init/dispose methods will not be invoked.', + correctionMessage: 'You should either use `$_asyncDepKeyword` declaration ' + 'or do not implement AsyncLifecycle interface.', + errorSeverity: ErrorSeverity.WARNING, + ); + + const UseAsyncDepForAsyncLifecycle() : super(code: _code); + + @override + void run( + CustomLintResolver resolver, + ErrorReporter reporter, + CustomLintContext context, + ) { + yxScopeRegistry(context).addScopeDeclarations((module) { + void checkAsyncDeps(BaseScopeDeclaration module) { + for (final dep in module.deps.values) { + if (dep.isAsync) { + continue; + } + final methodInvocation = dep.field.fields.childEntities + .whereType() + .expand((e) => e.childEntities.whereType()) + .first; + final depClass = (methodInvocation.staticType as InterfaceType) + .typeArguments + .map((e) => e.element) + .whereType() + .first; + final implementsAsyncLifecycle = + asyncLifecycleType.isAssignableFromType(depClass.thisType); + if (implementsAsyncLifecycle) { + reporter.atToken( + methodInvocation.methodName.token, + _code, + data: methodInvocation, + ); + } + } + for (final module in module.modules.values) { + checkAsyncDeps(module); + } + } + + checkAsyncDeps(module); + }); + } + + @override + List getFixes() => [UseAsyncDepForAsyncLifecycleFix()]; +} + +class UseAsyncDepForAsyncLifecycleFix extends DartFix { + @override + void run( + CustomLintResolver resolver, + ChangeReporter reporter, + CustomLintContext context, + AnalysisError analysisError, + List others, + ) { + final builder = reporter.createChangeBuilder( + message: 'Use `asyncDep` declaration', + priority: FixPriority.useAsyncDepForAsyncLifecycle.value, + ); + final methodInvocation = analysisError.data as MethodInvocation; + + builder.addDartFileEdit((builder) { + builder.addSimpleReplacement( + methodInvocation.methodName.sourceRange, + 'asyncDep', + ); + }); + } +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/lib/src/models/dep.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/lib/src/models/dep.dart new file mode 100644 index 0000000..17b46b3 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/lib/src/models/dep.dart @@ -0,0 +1,74 @@ +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/token.dart'; +import 'package:analyzer/dart/element/type.dart'; +import 'package:yx_scope_linter/src/types.dart'; + +class DepDeclaration { + final FieldDeclaration field; + final Token nameToken; + final DartType type; + final BaseScopeDeclaration parent; + final Map deps = {}; + + DepDeclaration({ + required this.field, + required this.nameToken, + required this.type, + required this.parent, + }); + + String get name => nameToken.lexeme; + + void addDep(DepDeclaration dep) => deps[dep.name] = dep; + + bool get isSync => depValueType.isExactlyType(type); + + bool get isAsync => asyncDepValueType.isExactlyType(type); + + @override + String toString() => name; +} + +abstract class BaseScopeDeclaration { + final ClassDeclaration node; + + final Map deps = {}; + final Map modules = {}; + + BaseScopeDeclaration({required this.node}); + + DartType get type => node.declaredElement!.thisType; + + addDep(DepDeclaration dep) => deps[dep.name] = dep; + addModule(ModuleDeclaration module) => modules[module.name] = module; + + bool get isRoot => this is ScopeDeclaration; + bool get isModule => this is ModuleDeclaration; +} + +class ScopeDeclaration extends BaseScopeDeclaration { + ScopeDeclaration({required super.node}); + final List> initializeQueue = []; + + void addScopeQueue() => initializeQueue.add({}); + void addDepToQueue(DepDeclaration dep) => initializeQueue.last.add(dep); + + @override + String toString() => ''; +} + +class ModuleDeclaration extends BaseScopeDeclaration { + final BaseScopeDeclaration parent; + final Token? nameToken; + + ModuleDeclaration({ + required this.parent, + required Token this.nameToken, + required super.node, + }); + + String get name => nameToken?.lexeme ?? ''; + + @override + String toString() => '$parent$name.'; +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/lib/src/models/exceptions.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/lib/src/models/exceptions.dart new file mode 100644 index 0000000..a2f5130 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/lib/src/models/exceptions.dart @@ -0,0 +1,5 @@ +class ScopeLinterError implements Exception { + final String message; + + const ScopeLinterError(this.message); +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/lib/src/names.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/lib/src/names.dart new file mode 100644 index 0000000..7c853ef --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/lib/src/names.dart @@ -0,0 +1,5 @@ +class MethodNames { + static const initializeQueue = 'initializeQueue'; + + const MethodNames._(); +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/lib/src/plugin.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/lib/src/plugin.dart new file mode 100644 index 0000000..4b85146 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/lib/src/plugin.dart @@ -0,0 +1,20 @@ +import 'package:custom_lint_builder/custom_lint_builder.dart'; +import 'package:yx_scope_linter/src/lints/avoid_async_dep_child_scope.dart'; +import 'package:yx_scope_linter/src/lints/consider_dep_suffix.dart'; +import 'package:yx_scope_linter/src/lints/dep_cycle.dart'; +import 'package:yx_scope_linter/src/lints/pass_async_lifecycle_in_initialize_queue.dart'; +import 'package:yx_scope_linter/src/lints/use_async_dep_for_async_lifecycle.dart'; + +import 'lints/final_dep.dart'; + +class YXScopeLintsPlugin extends PluginBase { + @override + List getLintRules(CustomLintConfigs configs) => const [ + FinalDep(), + DepCycle(), + ConsiderDepSuffix(), + PassAsyncLifecycleInInitializeQueue(), + UseAsyncDepForAsyncLifecycle(), + AvoidChildScopeInInitializeQueue(), + ]; +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/lib/src/priority.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/lib/src/priority.dart new file mode 100644 index 0000000..224e4a2 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/lib/src/priority.dart @@ -0,0 +1,7 @@ +enum FixPriority { + useAsyncDepForAsyncLifecycle, + finalDep, + considerDepSuffix; + + int get value => 100 - index; +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/lib/src/resolved_yx_scope_result.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/lib/src/resolved_yx_scope_result.dart new file mode 100644 index 0000000..faf1eee --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/lib/src/resolved_yx_scope_result.dart @@ -0,0 +1,68 @@ +import 'dart:async'; + +import 'package:analyzer/dart/analysis/results.dart'; +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/element.dart'; +import 'package:yx_scope_linter/src/names.dart'; + +import 'models/dep.dart'; +import 'types.dart'; +import 'yx_scope_registry.dart'; + +part 'visitors/parse_visitor.dart'; +part 'visitors/parse_dependencies_for_dep_visitor.dart'; +part 'visitors/parse_scope_declaration_visitor.dart'; +part 'visitors/parse_initialize_queue_visitor.dart'; + +/// Class that analyzes all files and stores parsed data for analysis +class ResolvedYXScopeResult { + final List units; + ResolvedYXScopeResult._(this.units); + + static Future from( + List units, + ) async { + final result = ResolvedYXScopeResult._(units); + final visitor = _ParseVisitor(result); + + for (final unit in units) { + /// Skip generated files during parsing + const generatedExtensions = {'.freezed.dart', '.g.dart'}; + final shortName = unit.declaredElement?.source.shortName ?? ''; + if (generatedExtensions.any(shortName.endsWith)) { + continue; + } + await unit.accept(visitor); + } + + return result; + } + + /// Units contain resolved data that can be used to extract ClassDeclaration + /// from [element] + ClassDeclaration? classDeclarationByElement(Element element) { + for (final unit in units) { + final declarations = unit.declarations + .where((declaration) => declaration.declaredElement == element) + .whereType(); + if (declarations.isEmpty) { + return null; + } + return declarations.first; + } + return null; + } + + final scopeDeclarations = {}; + + void accept(YXScopeRegistryVisitor visitor) { + visitor.visitResolvedUnits(this); + } + + void visitChildren(YXScopeRegistryVisitor visitor) { + for (final scopeDeclaration in scopeDeclarations) { + visitor.visitScopeDeclaration(scopeDeclaration); + } + } +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/lib/src/types.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/lib/src/types.dart new file mode 100644 index 0000000..4332801 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/lib/src/types.dart @@ -0,0 +1,40 @@ +import 'package:custom_lint_builder/custom_lint_builder.dart'; + +const baseScopeContainerType = TypeChecker.fromName( + 'BaseScopeContainer', + packageName: 'yx_scope', +); + +const depValueType = TypeChecker.fromName( + 'Dep', + packageName: 'yx_scope', +); +const asyncDepValueType = TypeChecker.fromName( + 'AsyncDep', + packageName: 'yx_scope', +); + +const asyncLifecycleType = TypeChecker.fromName( + 'AsyncLifecycle', + packageName: 'yx_scope', +); + +const childScopeHolderValueType = TypeChecker.fromName( + 'ChildScopeHolder', + packageName: 'yx_scope', +); + +const scopeContainerValueType = TypeChecker.fromName( + 'ScopeContainer', + packageName: 'yx_scope', +); + +const anyDepValueTypes = TypeChecker.any([ + depValueType, + asyncDepValueType, +]); + +const scopeModuleType = TypeChecker.fromName( + 'ScopeModule', + packageName: 'yx_scope', +); diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/lib/src/utils.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/lib/src/utils.dart new file mode 100644 index 0000000..f767e3b --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/lib/src/utils.dart @@ -0,0 +1,37 @@ +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/element/element.dart'; + +import 'types.dart'; + +/// Utility class for working with class declarations and elements +class ClassUtils { + /// Checks if a class implements the specified interface by name + static bool implementsInterface(ClassElement element, String ancestorName) => + element.interfaces + .map((e) => e.getDisplayString()) + .contains(ancestorName); + + /// Determines if a class declaration is a scope container + static bool isScopeContainer(ClassDeclaration node) { + final element = node.declaredElement; + return element != null + ? baseScopeContainerType.isAssignableFrom(element) + : false; + } + + /// Gets all non-static field declarations from a class + static Iterable getInstanceFields(ClassDeclaration node) { + return node.members + .whereType() + .where((element) => !element.isStatic); + } + + /// Gets all non-static method declarations from a class + static Iterable getInstanceMethods(ClassDeclaration node) { + return node.members + .whereType() + .where((element) => !element.isStatic); + } + + const ClassUtils._(); +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/lib/src/visitors/parse_dependencies_for_dep_visitor.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/lib/src/visitors/parse_dependencies_for_dep_visitor.dart new file mode 100644 index 0000000..45f2fb3 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/lib/src/visitors/parse_dependencies_for_dep_visitor.dart @@ -0,0 +1,103 @@ +part of '../resolved_yx_scope_result.dart'; + +/// Visitor that analyzes and establishes dependency relationships between +/// different dependencies within a [scope] hierarchy. +class _ParseDependenciesForDepVisitor extends GeneralizingAstVisitor { + final BaseScopeDeclaration scope; + DepDeclaration? dep; + + /// Tracks the current scope during traversal (changes when processing modules) + late BaseScopeDeclaration _curScope = scope; + + _ParseDependenciesForDepVisitor(this.scope, this.dep); + + /// Main execution method that processes all dependencies and modules + Future run() async { + // Process all direct dependencies of the current scope + for (var dep in scope.deps.values) { + this.dep = dep; + await dep.field.fields.variables.first.accept(this); + } + + // Recursively process all module dependencies + for (var module in scope.modules.values) { + await _ParseDependenciesForDepVisitor(module, null).run(); + } + } + + @override + Future? visitNode(AstNode node) async { + // Default visitor behavior - process all child nodes + for (final node in node.childEntities.whereType()) { + await node.accept(this); + } + } + + @override + Future? visitSimpleIdentifier(SimpleIdentifier node) async { + final type = node.staticType; + if (type == null) { + return; + } + + // Handle scope container references + if (baseScopeContainerType.isAssignableFromType(type)) { + if (_curScope is ModuleDeclaration) { + final moduleParent = (_curScope as ModuleDeclaration).parent; + if (type == moduleParent.type) { + _curScope = moduleParent; + } + } + } + + // Handle module references - update current scope context + if (scopeModuleType.isAssignableFromType(type)) { + final curScope = _curScope.modules[node.name]; + if (curScope != null) { + _curScope = curScope; + } + } + + // Handle method calls that return dependency types + if (node.staticElement is MethodElement) { + final methodElement = node.staticElement as MethodElement; + + if (anyDepValueTypes.isExactlyType(methodElement.returnType)) { + return; + } + + // Verify the method is in the same library as the current scope + final element = node.staticElement; + final library = element?.library; + final scopeLibrary = _curScope.node.declaredElement?.library; + + if (element == null || library == null || scopeLibrary == null) { + return; + } + + if (library != scopeLibrary) { + return; + } + + // Process the method declaration to find nested dependencies + final result = (await library.session + .getResolvedLibraryByElement(library)) as ResolvedLibraryResult; + final methodDeclaration = + result.getElementDeclaration(element)!.node as MethodDeclaration; + + await _ParseDependenciesForDepVisitor(_curScope, dep) + .visitMethodDeclaration(methodDeclaration); + + _curScope = scope; + } + + // Handle direct dependency references + if (anyDepValueTypes.isAssignableFromType(type)) { + final foundDep = _curScope.deps[node.name]; + if (foundDep != null && dep != null) { + dep!.addDep(foundDep); + } + _curScope = scope; + } + } +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/lib/src/visitors/parse_initialize_queue_visitor.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/lib/src/visitors/parse_initialize_queue_visitor.dart new file mode 100644 index 0000000..1160e9c --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/lib/src/visitors/parse_initialize_queue_visitor.dart @@ -0,0 +1,60 @@ +part of '../resolved_yx_scope_result.dart'; + +/// Visitor that parses the dependency initialization queue +/// for a [scope] declaration by analyzing the initializeQueue method. +class _ParseInitializeQueueVisitor extends SimpleAstVisitor { + final ScopeDeclaration scope; + + /// Tracks the current scope during traversal + late BaseScopeDeclaration _curScope = scope; + + _ParseInitializeQueueVisitor(this.scope); + + @override + void visitListLiteral(ListLiteral node) { + // Process all elements in a list literal + node.visitChildren(this); + } + + @override + void visitSetOrMapLiteral(SetOrMapLiteral node) { + // Create a new queue section for each set/map literal + scope.addScopeQueue(); + node.visitChildren(this); + } + + @override + void visitSimpleIdentifier(SimpleIdentifier node) { + final type = node.staticType; + if (type == null) { + return; + } + + // Handle module references - update current scope context + if (scopeModuleType.isAssignableFromType(type)) { + final curScope = _curScope.modules[node.name]; + if (curScope != null) { + _curScope = curScope; + } + } + + // Handle dependency references - add to initialization queue + if (anyDepValueTypes.isAssignableFromType(type)) { + final dep = _curScope.deps[node.name]; + if (dep != null) { + scope.addDepToQueue(dep); + } + _curScope = scope; + } + } + + @override + void visitMethodDeclaration(MethodDeclaration node) { + // Only process the initializeQueue method + if (node.declaredElement?.name != MethodNames.initializeQueue) { + return; + } + // Process the method body to find dependencies + node.body.visitChildren(this); + } +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/lib/src/visitors/parse_scope_declaration_visitor.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/lib/src/visitors/parse_scope_declaration_visitor.dart new file mode 100644 index 0000000..a9a596e --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/lib/src/visitors/parse_scope_declaration_visitor.dart @@ -0,0 +1,74 @@ +part of '../resolved_yx_scope_result.dart'; + +/// Visitor that parses scope declarations and their dependencies +/// and put them into [declaration] +class _ParseScopeDeclaration extends SimpleAstVisitor { + final BaseScopeDeclaration declaration; + + _ParseScopeDeclaration(this.declaration); + + @override + Future visitClassDeclaration(ClassDeclaration node) => + Future.wait(node.members.map((e) async => e.accept(this))); + + @override + Future visitFieldDeclaration(FieldDeclaration node) => + Future.wait(node.fields.variables.map((e) async => e.accept(this))); + + @override + Future visitVariableDeclaration(VariableDeclaration node) async { + final element = node.declaredElement; + if (element == null) { + return; + } + + // Handle dependency declarations + if (anyDepValueTypes.isExactlyType(element.type)) { + declaration.addDep( + DepDeclaration( + field: node.thisOrAncestorOfType()!, + nameToken: node.name, + type: element.type, + parent: declaration, + ), + ); + } + + // Handle scope module references + if (scopeModuleType.isAssignableFromType(element.type)) { + await Future.wait(node.childEntities + .whereType() + .map((e) async => e.accept(this))); + } + } + + @override + Future visitInstanceCreationExpression( + InstanceCreationExpression node) async { + final library = node.constructorName.type.element?.library; + if (library == null) { + return; + } + + // Resolve the module declaration from its library + final result = (await library.session.getResolvedLibraryByElement(library)) + as ResolvedLibraryResult; + + final moduleNode = result + .getElementDeclaration( + node.constructorName.type.element as ClassElement)! + .node as ClassDeclaration; + + // Create and register the module declaration + final moduleDeclaration = ModuleDeclaration( + parent: declaration, + nameToken: node.thisOrAncestorOfType()!.name, + node: moduleNode, + ); + + declaration.addModule(moduleDeclaration); + + // Recursively parse the module's contents + await moduleNode.accept(_ParseScopeDeclaration(moduleDeclaration)); + } +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/lib/src/visitors/parse_visitor.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/lib/src/visitors/parse_visitor.dart new file mode 100644 index 0000000..9556272 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/lib/src/visitors/parse_visitor.dart @@ -0,0 +1,40 @@ +part of '../resolved_yx_scope_result.dart'; + +/// Parser that stores parsed data in [result] +class _ParseVisitor extends SimpleAstVisitor { + final ResolvedYXScopeResult result; + + _ParseVisitor(this.result); + + @override + Future visitCompilationUnit(CompilationUnit node) => + Future.wait(node.declarations.map((e) async => e.accept(this))); + + @override + Future visitClassDeclaration(ClassDeclaration node) async { + final element = node.declaredElement; + if (element == null) { + return; + } + + // Skip if not a scope container or module type + if (!baseScopeContainerType.isAssignableFromType(element.thisType) && + !scopeModuleType.isAssignableFromType(element.thisType)) { + return; + } + + // Parse the scope declaration + final visitor = _ParseScopeDeclaration(ScopeDeclaration(node: node)); + await visitor.visitClassDeclaration(node); + + // Parse initialization queue members + node.members.accept( + _ParseInitializeQueueVisitor(visitor.declaration as ScopeDeclaration)); + + // Parse dependencies + await _ParseDependenciesForDepVisitor(visitor.declaration, null).run(); + + // Add the completed declaration to results + result.scopeDeclarations.add(visitor.declaration as ScopeDeclaration); + } +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/lib/src/yx_scope_lint_rule.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/lib/src/yx_scope_lint_rule.dart new file mode 100644 index 0000000..f168eeb --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/lib/src/yx_scope_lint_rule.dart @@ -0,0 +1,52 @@ +import 'package:custom_lint_builder/custom_lint_builder.dart'; + +import 'resolved_yx_scope_result.dart'; +import 'yx_scope_registry.dart'; + +/// A base [DartLintRule] that parses required classes +/// and provides data for analysis +/// +/// Check [YXScopeRegistry] to see what can be tracked +abstract class YXScopeLintRule extends DartLintRule { + const YXScopeLintRule({required super.code}); + + static final _contextKey = Object(); + + @override + Future startUp( + CustomLintResolver resolver, + CustomLintContext context, + ) async { + await _setup(resolver, context); + await super.startUp(resolver, context); + } + + YXScopeRegistry yxScopeRegistry(CustomLintContext context) { + final registry = context.sharedState[_contextKey] as YXScopeRegistry?; + if (registry == null) { + throw StateError('YXScopeRegistry not initialized'); + } + return registry; + } + + Future _setup( + CustomLintResolver resolver, + CustomLintContext context, + ) async { + final registry = context.sharedState[_contextKey] = YXScopeRegistry(); + + final watch = Stopwatch()..start(); + + final unit = await resolver.getResolvedUnitResult(); + // Here we parse everything we need + final result = await ResolvedYXScopeResult.from([unit.unit]); + + watch.stop(); + print(': ${watch.elapsedMilliseconds}ms'); + + context.addPostRunCallback(() { + // Here we store the parsed data and notify subscribers + registry.run(result); + }); + } +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/lib/src/yx_scope_registry.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/lib/src/yx_scope_registry.dart new file mode 100644 index 0000000..8792d1e --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/lib/src/yx_scope_registry.dart @@ -0,0 +1,34 @@ +import 'models/dep.dart'; +import 'resolved_yx_scope_result.dart'; + +/// An entity that provides the ability to track parsed data +class YXScopeRegistry { + /// This method provides the parsing result to subscribers + void run(ResolvedYXScopeResult result) { + result.accept(YXScopeRegistryVisitor(this)); + } + + final List _forScopeDeclarations = []; + + /// This method provides the ability to track parsed scope classes + void addScopeDeclarations(void Function(ScopeDeclaration scope) listener) { + _forScopeDeclarations.add(listener); + } +} + +/// This visitor simply notifies all subscribers +class YXScopeRegistryVisitor { + final YXScopeRegistry _registry; + + YXScopeRegistryVisitor(this._registry); + + void visitResolvedUnits(ResolvedYXScopeResult result) { + result.visitChildren(this); + } + + void visitScopeDeclaration(ScopeDeclaration result) { + for (final listener in _registry._forScopeDeclarations) { + listener(result); + } + } +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/lib/yx_scope_linter.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/lib/yx_scope_linter.dart new file mode 100644 index 0000000..f78cd15 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/lib/yx_scope_linter.dart @@ -0,0 +1,7 @@ +library yx_scope_linter; + +import 'package:custom_lint_builder/custom_lint_builder.dart'; + +import 'src/plugin.dart'; + +PluginBase createPlugin() => YXScopeLintsPlugin(); diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/pubspec.yaml b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/pubspec.yaml new file mode 100644 index 0000000..e159b32 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/pubspec.yaml @@ -0,0 +1,22 @@ +name: yx_scope_linter +description: A package that contains static analysis rules for yx_scope DI framework +version: 0.1.4 +repository: https://github.com/yandex/city-services-pub/tree/main/yx_scope/packages/yx_scope_linter +issue_tracker: https://github.com/yandex/city-services-pub/issues +topics: + - di + - scope + - dependency-injection + - dependency-management + +environment: + sdk: '>=2.19.6 <4.0.0' + +dependencies: + analyzer: ^6.4.1 + analyzer_plugin: ^0.11.2 + custom_lint_builder: ^0.6.2 + yx_scope: ^1.1.1 + +dev_dependencies: + lints: ^2.0.0 diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/pubspec_overrides.yaml b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/pubspec_overrides.yaml new file mode 100644 index 0000000..d249dfc --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_scope/packages/yx_scope_linter/pubspec_overrides.yaml @@ -0,0 +1,3 @@ +dependency_overrides: + yx_scope: + path: ../yx_scope diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/.gitignore b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/.gitignore new file mode 100644 index 0000000..8267c1b --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/.gitignore @@ -0,0 +1,33 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +**/doc/api/ +.dart_tool/ +build/ +/pubspec.lock +.arcignore +.clineignore +.cursorignore +.rooignore diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/AUTHORS b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/AUTHORS new file mode 100644 index 0000000..0280e4e --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/AUTHORS @@ -0,0 +1,13 @@ +The following authors have created the source code of "yx_state" published and distributed by YANDEX LLC as the owner: +kltsv +guid-empty +Vorkytaka +sorokinDev +wwwhttpru + +The following authors have licensed their contributions to YANDEX LLC and everyone who uses "yx_state" under the licensing terms detailed in LICENSE available at https://github.com/yandex/yx_state/blob/main/LICENSE. +kltsv +guid-empty +Vorkytaka +sorokinDev +wwwhttpru diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/CONTRIBUTING.md b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/CONTRIBUTING.md new file mode 100644 index 0000000..3cef4d8 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/CONTRIBUTING.md @@ -0,0 +1,31 @@ +## Notice to external contributors +### General info +Hello! In order for us (YANDEX LLC) to accept patches and other contributions from you, you will have to adopt our Contributor License Agreement (the “CLA”). The current version of the CLA you may find here: + +* https://yandex.ru/legal/cla/?lang=en (in English) +* https://yandex.ru/legal/cla/?lang=ru (in Russian). + +By adopting the CLA, you state the following: + +* You obviously wish and are willingly licensing your contributions to us for our open source projects under the terms of the CLA, +* You have read the terms and conditions of the CLA and agree with them in full, +* You are legally able to provide and license your contributions as stated, +* We may use your contributions for our open source projects and for any other our project too, +* We rely on your assurances concerning the rights of third parties in relation to your contributions. + +If you agree with these principles, please read and adopt our CLA. By providing us your contributions, you hereby declare that you have read and adopted our CLA, and we may freely merge your contributions with our corresponding open source project and use it in further in accordance with terms and conditions of the CLA. + +### Provide contributions +If you have adopted terms and conditions of the CLA, you are able to provide your contributions. When you submit your pull request, please add the following information into it: + +``` +I hereby agree to the terms of the CLA available at: [link]. +``` + +Replace the bracketed text as follows: + +* [link] is the link at the current version of the CLA (you may add here a link https://yandex.ru/legal/cla/?lang=en (in English) or a link https://yandex.ru/legal/cla/?lang=ru (in Russian). + It is enough to provide us with such notification once. + +### Other questions +If you have any questions, please write us at opensource@yandex-team.ru. diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/LICENSE b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/LICENSE new file mode 100644 index 0000000..1734596 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2025 YANDEX LLC + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/README.md b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/README.md new file mode 100644 index 0000000..683144a --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/README.md @@ -0,0 +1,11 @@ +# yx_state packages + +State management library for Dart / Flutter. + +## Library Components + +The library group currently consists of: + +- **[yx_state](packages/yx_state)**: The core library for state management +- **[yx_state_flutter](packages/yx_state_flutter)**: Flutter widgets for yx_state +- **[yx_state_transformers](packages/yx_state_transformers)**: Transformers for yx_state diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/analysis_options.yaml b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/analysis_options.yaml new file mode 100644 index 0000000..df7782d --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/analysis_options.yaml @@ -0,0 +1,92 @@ +formatter: + page_width: 80 + trailing_commas: preserve + +linter: + rules: + - avoid_empty_else + - avoid_relative_lib_imports + - avoid_shadowing_type_parameters + - avoid_types_as_parameter_names + - await_only_futures + - camel_case_extensions + - camel_case_types + - collection_methods_unrelated_type + - curly_braces_in_flow_control_structures + - dangling_library_doc_comments + - depend_on_referenced_packages + - empty_catches + - file_names + - hash_and_equals + - implicit_call_tearoffs + - library_annotations + - no_duplicate_case_values + - no_wildcard_variable_uses + - non_constant_identifier_names + - null_check_on_nullable_type_parameter + - prefer_generic_function_type_aliases + - prefer_is_empty + - prefer_is_not_empty + - prefer_iterable_whereType + - prefer_typing_uninitialized_variables + - provide_deprecation_message + - secure_pubspec_urls + - type_literal_in_constant_pattern + - unnecessary_overrides + - unrelated_type_equality_checks + - use_string_in_part_of_directives + - valid_regexps + - void_checks + - annotate_overrides + - avoid_function_literals_in_foreach_calls + - avoid_init_to_null + - avoid_renaming_method_parameters + - avoid_return_types_on_setters + - avoid_returning_null_for_void + - avoid_single_cascade_in_expression_statements + - constant_identifier_names + - control_flow_in_finally + - empty_constructor_bodies + - empty_statements + - exhaustive_cases + - implementation_imports + - library_prefixes + - library_private_types_in_public_api + - no_leading_underscores_for_library_prefixes + - no_leading_underscores_for_local_identifiers + - null_closures + - overridden_fields + - package_names + - prefer_adjacent_string_concatenation + - prefer_collection_literals + - prefer_conditional_assignment + - prefer_contains + - prefer_final_fields + - prefer_for_elements_to_map_fromIterable + - prefer_function_declarations_over_variables + - prefer_if_null_operators + - prefer_initializing_formals + - prefer_inlined_adds + - prefer_interpolation_to_compose_strings + - prefer_is_not_operator + - prefer_null_aware_operators + - prefer_spread_collections + - recursive_getters + - slash_for_doc_comments + - type_init_formals + - unnecessary_brace_in_string_interps + - unnecessary_const + - unnecessary_constructor_name + - unnecessary_getters_setters + - unnecessary_late + - unnecessary_new + - unnecessary_null_aware_assignments + - unnecessary_null_in_if_null_operators + - unnecessary_nullable_for_final_variable_declarations + - unnecessary_string_escapes + - unnecessary_string_interpolations + - unnecessary_this + - unnecessary_to_list_in_spreads + - use_function_type_syntax_for_parameters + - use_rethrow_when_possible + - use_super_parameters diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/assets/logos/yx_state.webp b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/assets/logos/yx_state.webp new file mode 100644 index 0000000..84fa3c5 Binary files /dev/null and b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/assets/logos/yx_state.webp differ diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state/.gitignore b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state/.gitignore new file mode 100644 index 0000000..8267c1b --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state/.gitignore @@ -0,0 +1,33 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +**/doc/api/ +.dart_tool/ +build/ +/pubspec.lock +.arcignore +.clineignore +.cursorignore +.rooignore diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state/AUTHORS b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state/AUTHORS new file mode 100644 index 0000000..0280e4e --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state/AUTHORS @@ -0,0 +1,13 @@ +The following authors have created the source code of "yx_state" published and distributed by YANDEX LLC as the owner: +kltsv +guid-empty +Vorkytaka +sorokinDev +wwwhttpru + +The following authors have licensed their contributions to YANDEX LLC and everyone who uses "yx_state" under the licensing terms detailed in LICENSE available at https://github.com/yandex/yx_state/blob/main/LICENSE. +kltsv +guid-empty +Vorkytaka +sorokinDev +wwwhttpru diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state/CHANGELOG.md b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state/CHANGELOG.md new file mode 100644 index 0000000..effe43c --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state/CHANGELOG.md @@ -0,0 +1,3 @@ +## 1.0.0 + +- Initial version. diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state/CONTRIBUTING.md b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state/CONTRIBUTING.md new file mode 100644 index 0000000..3cef4d8 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state/CONTRIBUTING.md @@ -0,0 +1,31 @@ +## Notice to external contributors +### General info +Hello! In order for us (YANDEX LLC) to accept patches and other contributions from you, you will have to adopt our Contributor License Agreement (the “CLA”). The current version of the CLA you may find here: + +* https://yandex.ru/legal/cla/?lang=en (in English) +* https://yandex.ru/legal/cla/?lang=ru (in Russian). + +By adopting the CLA, you state the following: + +* You obviously wish and are willingly licensing your contributions to us for our open source projects under the terms of the CLA, +* You have read the terms and conditions of the CLA and agree with them in full, +* You are legally able to provide and license your contributions as stated, +* We may use your contributions for our open source projects and for any other our project too, +* We rely on your assurances concerning the rights of third parties in relation to your contributions. + +If you agree with these principles, please read and adopt our CLA. By providing us your contributions, you hereby declare that you have read and adopted our CLA, and we may freely merge your contributions with our corresponding open source project and use it in further in accordance with terms and conditions of the CLA. + +### Provide contributions +If you have adopted terms and conditions of the CLA, you are able to provide your contributions. When you submit your pull request, please add the following information into it: + +``` +I hereby agree to the terms of the CLA available at: [link]. +``` + +Replace the bracketed text as follows: + +* [link] is the link at the current version of the CLA (you may add here a link https://yandex.ru/legal/cla/?lang=en (in English) or a link https://yandex.ru/legal/cla/?lang=ru (in Russian). + It is enough to provide us with such notification once. + +### Other questions +If you have any questions, please write us at opensource@yandex-team.ru. diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state/LICENSE b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state/LICENSE new file mode 100644 index 0000000..1734596 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2025 YANDEX LLC + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state/README.md b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state/README.md new file mode 100644 index 0000000..a83c7cf --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state/README.md @@ -0,0 +1,142 @@ +# yx_state + +
+ +The yx_state package logo + +**A state management library for Dart/Flutter applications.** + +[![Pub Version](https://img.shields.io/pub/v/yx_state)](https://pub.dev/packages/yx_state) + +
+ +--- + +## 📦 Installation + +Add this package to your `pubspec.yaml` file: + +```yaml +dependencies: + yx_state: +``` + +## 🚀 Quick Start + +### Basic Counter Example + +```dart +import 'package:yx_state/yx_state.dart'; + +// Define your state +class CounterState { + final int count; + + const CounterState(this.count); + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + return other is CounterState && other.count == count; + } + + @override + int get hashCode => count.hashCode; +} + +// Create a state manager +class CounterManager extends StateManager { + CounterManager() : super(const CounterState(0)); + + void increment() => handle((emit) async { + emit(CounterState(state.count + 1)); + }); + + void decrement() => handle((emit) async { + emit(CounterState(state.count - 1)); + }); +} + +// Use the state manager +void main() { + final counter = CounterManager(); + + // Listen to state changes + counter.stream.listen((state) { + print('Count: ${state.count}'); + }); + + // Trigger state changes + counter.increment(); // Output: Count: 1 + counter.increment(); // Output: Count: 2 + counter.decrement(); // Output: Count: 1 + + // Clean up resources when done + counter.close(); +} +``` + +## 🔧 Features + +### State Observers + +Monitor state changes for debugging or analytics: + +```dart +class MyCustomObserver extends StateManagerObserver { + const MyCustomObserver(); + + @override + void onChange( + StateManagerBase stateManager, + Object? currentState, + Object? nextState, + Object? identifier, + ) { + print( + 'State changed from $currentState to $nextState with ' + 'identifier: $identifier', + ); + super.onChange(stateManager, currentState, nextState, identifier); + } +} + +void main() { + // Set the observer globally + StateManagerOverrides.observer = const MyCustomObserver(); +} +``` + +### Global Overrides + +Customize behavior globally: + +```dart +void main() { + // Set the shouldEmit globally + StateManagerOverrides.defaultShouldEmit = (current, next) => true; +} +``` + +### Error Handling + +Comprehensive error handling with built-in support: + +```dart +class ErrorHandlingStateManager extends StateManager { + ErrorHandlingStateManager() : super(MyState.initial()); + + Future performOperation() => handle((emit) async { + try { + ... + // Risky operation + final result = await someApiCall(); + emit(MyState.success(data: result)); + } catch (error, stackTrace) { + // Report error through the state manager + addError(error, stackTrace); + ... + } + }); +} +``` diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state/analysis_options.yaml b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state/analysis_options.yaml new file mode 100644 index 0000000..f04c6cf --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state/analysis_options.yaml @@ -0,0 +1 @@ +include: ../../analysis_options.yaml diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state/example/main.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state/example/main.dart new file mode 100644 index 0000000..b1eb57f --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state/example/main.dart @@ -0,0 +1,101 @@ +import 'package:yx_state/yx_state.dart'; + +void main() { + // Set the observer globally + StateManagerOverrides.observer = const MyCustomObserver(); + + // Create a new state manager + final counter = CounterStateManager(0); + + counter.increment(); + counter.decrement(); + counter.someLogic(); + counter.increment(); + counter.incrementBy(5); + + // Close the state manager + counter.close(); +} + +class CounterStateManager extends StateManager { + CounterStateManager(super.state); + + void increment() => handle((emit) async { + await Future.delayed(const Duration(seconds: 1)); + emit(state + 1); + }, identifier: 'increment'); + + void incrementBy(int value) => handle((emit) async { + await Future.delayed(const Duration(seconds: 1)); + emit(state + value); + }, identifier: {'value': value}); + + void decrement() => handle((emit) async { + await Future.delayed(const Duration(seconds: 1)); + emit(state - 1); + }, identifier: 'decrement'); + + void someLogic() => handle((emit) async { + try { + if (state == 0) { + throw Exception(); + } + + emit(state); + } on Object catch (error, sk) { + addError(error, sk, 'someLogic'); + } + + emit(0); + }, identifier: 'someLogic'); +} + +class MyCustomObserver extends StateManagerObserver { + const MyCustomObserver(); + + @override + void onChange( + StateManagerBase stateManager, + Object? currentState, + Object? nextState, + Object? identifier, + ) { + print( + 'State changed from $currentState to $nextState ' + 'identifier: $identifier', + ); + super.onChange(stateManager, currentState, nextState, identifier); + } + + @override + void onHandleStart( + StateManagerBase stateManager, + Object? identifier, + ) { + print('Handle started: $identifier'); + super.onHandleStart(stateManager, identifier); + } + + @override + void onHandleDone( + StateManagerBase stateManager, + Object? identifier, + ) { + print('Handle done: $identifier'); + super.onHandleDone(stateManager, identifier); + } + + @override + void onError( + StateManagerBase stateManager, + Object error, + StackTrace stackTrace, + Object? identifier, + ) { + print( + 'Oops error: $error, stackTrace: $stackTrace ' + 'identifier: $identifier', + ); + super.onError(stateManager, error, stackTrace, identifier); + } +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state/lib/src/base/interface.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state/lib/src/base/interface.dart new file mode 100644 index 0000000..8f6fc15 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state/lib/src/base/interface.dart @@ -0,0 +1,155 @@ +import 'package:meta/meta.dart'; + +/// An interface for objects that provide read access to a state stream. +/// +/// This interface provides the core capabilities for reading state from a state manager: +/// - Access to the current state value via [state] +/// - Access to a stream of state changes via [stream] +/// +/// This allows for both reactive (stream-based) and imperative (direct access) +/// consumption of state. +abstract class StateReadable { + /// A stream of state changes. + /// + /// This stream emits a new value whenever the state changes. + /// The stream is a broadcast stream and can be listened to multiple times. + Stream get stream; + + /// The current state value. + /// + /// This getter always returns the most up-to-date state value. + State get state; +} + +/// Interface for objects that can emit new state values +/// and track completion status. +/// +/// See also: +/// +/// * [FunctionHandler] which has access to an [Emitter]. +abstract class Emitter { + /// Indicates whether this emitter should accept new states. + /// + /// Returns `true` if the emitter has been closed or completed, + /// and no more state updates should be accepted. + bool get isDone; + + /// Update the current state with a new [state] value. + /// + /// Asserts that the emitter is not done before emitting the state. + void call(State state); +} + +/// Function signature for asynchronous state management operations +/// that use an [Emitter]. +typedef EmitterHandler = Future Function( + Emitter emit, +); + +/// Callback type for updating state by providing a new [State] value. +typedef Emittable = void Function( + State state, +); + +/// Handles the processing of state update functions. +/// +/// Different implementations provide various concurrency strategies. +abstract class FunctionHandler implements Closable { + /// Executes the provided handler function with the necessary lifecycle callbacks. + /// + /// Throws [StateError] if called after this handler has been closed. + Future call( + EmitterHandler handler, + Object? identifier, { + required Emittable onEmit, + required void Function(Object error, StackTrace stackTrace) onError, + required void Function() onStart, + required void Function() onDone, + }); +} + +/// An interface for objects that can be closed and have a closure state. +/// +/// Classes implementing this interface should guarantee that once closed: +/// - No further operations should be performed on the object +/// - All resources should be released +/// - The [isClosed] property should return true +@internal +abstract class Closable { + /// Indicates whether the object has been closed. + /// + /// Returns: + /// - `true` if the object has been successfully closed + /// - `false` if the object is still active and operational + bool get isClosed; + + /// Closes the object and releases associated resources. + /// + /// This method should be idempotent - calling it multiple times + /// should have the same effect as calling it once. + /// + /// Returns: + /// A [Future] that completes when the closure operation finishes. + Future close(); +} + +/// An internal interface for handling state manager lifecycle events. +/// +/// This interface defines the core lifecycle methods that a state manager +/// needs to implement to properly track and report its state changes and +/// other important events. +/// +/// It's typically implemented by [StateManagerBase] and its subclasses. +@internal +abstract class StateManagerListener { + /// Called when the state manager is created. + void onCreate(); + + /// Called when a handler function starts execution. + /// + /// [identifier] - An optional identifier for the handler + void onStart(Object? identifier); + + /// Called when a handler function completes execution. + /// + /// [identifier] - An optional identifier for the handler + void onDone(Object? identifier); + + /// Called when the state changes. + /// + /// [currentState] - The previous state value + /// [nextState] - The new state value + /// [identifier] - An optional identifier for the handler + void onChange(State currentState, State nextState, Object? identifier); + + /// Called when an error occurs within the state manager. + /// + /// [error] - The error that was thrown + /// [stackTrace] - The stack trace associated with the error + /// [identifier] - An optional identifier for the handler + void onError(Object error, StackTrace stackTrace, Object? identifier); + + /// Called when the state manager is closed. + void onClose(); +} + +/// An internal interface for handling state update operations. +/// +/// This interface defines the core method for processing state updates +/// through handler functions. +/// +/// It's typically implemented by [StateManagerBase] and its subclasses. +@internal +abstract class StateManagerHandler { + /// Processes a state update handler function. + /// + /// This method is the central point for executing state update logic + /// and managing the lifecycle of state updates. + /// + /// [handler] - The handler function that will potentially update the state + /// [identifier] - An optional identifier for the handler + Future handle( + EmitterHandler handler, { + Object? identifier, + }); +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state/lib/src/base/state_manager_base.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state/lib/src/base/state_manager_base.dart new file mode 100644 index 0000000..b95a4cb --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state/lib/src/base/state_manager_base.dart @@ -0,0 +1,144 @@ +import 'dart:async'; + +import 'package:meta/meta.dart'; +import 'package:yx_state/src/state_manager_overrides.dart'; + +import 'interface.dart'; +part '../test_util/state_manager_base_test_util.dart'; + +/// Base implementation of a state manager. +abstract class StateManagerBase + implements + StateReadable, + StateManagerHandler, + StateManagerListener, + Closable { + /// Creates a new [StateManagerBase] with the provided initial [state] + /// and function [handler]. + StateManagerBase( + State state, { + required FunctionHandler handler, + }) : _state = state, + _functionHandler = handler { + onCreate(); + } + + final FunctionHandler _functionHandler; + late final _controller = StreamController.broadcast(); + State _state; + bool _isClosing = false; + + @nonVirtual + @override + bool get isClosed => _controller.isClosed; + + @override + State get state => _state; + + @override + Stream get stream => _controller.stream; + + @nonVirtual + @protected + @override + Future handle( + EmitterHandler handler, { + Object? identifier, + }) async { + try { + if (isClosed) { + throw StateError( + 'Cannot handle handler after the state manager ' + 'has been closed', + ); + } + await _functionHandler.call( + handler, + identifier, + onEmit: (state) => _emit(state, identifier), + onError: (error, stackTrace) => onError(error, stackTrace, identifier), + onStart: () => onStart(identifier), + onDone: () => onDone(identifier), + ); + } on Object catch (error, stackTrace) { + onError(error, stackTrace, identifier); + } + } + + /// Adds an error to the state manager. + /// + /// This method will call [onError] with the provided [error], [stackTrace], + /// and [identifier]. Use this method to report errors that occur outside of + /// the normal state update flow. + @protected + @mustCallSuper + void addError(Object error, [StackTrace? stackTrace, Object? identifier]) { + onError(error, stackTrace ?? StackTrace.current, identifier); + } + + /// Closes the state manager and releases all resources. + /// + /// After calling this method, the state manager will no longer accept + /// new state updates and all streams will be closed. + /// + /// This method is idempotent - calling it multiple times has the same effect + /// as calling it once. + @mustCallSuper + @override + Future close() async { + if (isClosed || _isClosing) { + return; + } + + _isClosing = true; + + try { + await _functionHandler.close(); + onClose(); + await _controller.close(); + } on Object catch (error, stackTrace) { + onError(error, stackTrace, null); + } finally { + _isClosing = false; + } + } + + /// Determines whether a state update should be emitted. + /// + /// By default, this method returns true if the current state is not equal + /// to the next state. Override this method to provide custom equality + /// checking logic for complex state objects. + @protected + bool shouldEmit(State current, State next) => + StateManagerOverrides.defaultShouldEmit(current, next); + + /// Emits a new state value. + /// + /// This method: + /// 1. Checks if the state manager is closed + /// 2. Determines if the new state should be emitted using [shouldEmit] + /// 3. Calls [onChange] with the current and next state + /// 4. Updates the internal state + /// 5. Adds the new state to the stream + /// + /// If an error occurs during this process, it will be reported via [onError]. + void _emit(State state, Object? identifier) { + try { + if (isClosed) { + throw StateError('Cannot emit new states after calling close'); + } + + final prev = _state; + if (!shouldEmit(prev, state)) { + return; + } + + onChange(_state, state, identifier); + _state = state; + _controller.add(state); + } on Object catch (error, sk) { + onError(error, sk, identifier); + rethrow; + } + } +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state/lib/src/function_stream_handler/handle_task.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state/lib/src/function_stream_handler/handle_task.dart new file mode 100644 index 0000000..45b7b94 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state/lib/src/function_stream_handler/handle_task.dart @@ -0,0 +1,101 @@ +import 'package:meta/meta.dart'; + +import '../base/interface.dart'; +import 'handle_task_emitter.dart'; + +/// Signature for a function which converts an incoming task +/// into an outbound stream of tasks. +/// +/// Used when defining custom [HandleTaskTransformer]s. +typedef HandleTaskMapper = Stream> + Function(HandleTask task); + +/// Used to change how tasks are processed. +/// +/// The transformer defines the concurrency behavior of the tasks. +typedef HandleTaskTransformer = Stream> + Function(Stream> tasks, HandleTaskMapper mapper); + +/// A task that can be handled by a function handler. +class HandleTask { + /// The identifier for the task. + final Object? identifier; + + /// The handler for the task. + final EmitterHandler _handler; + + /// The callback to call when the task starts. + final void Function() _onStart; + + /// The callback to call when the task is done. + final void Function() _onDone; + + /// The emitter for the task. + final HandleTaskEmitter _emitter; + + /// Returns true if the task is done. + bool get isDone => _emitter.isDone; + + /// Returns true if the task is canceled. + bool get isCanceled => _emitter.isCanceled; + + /// A future that completes when the task is done. + Future get future => _emitter.future; + + /// Creates a new [HandleTask] with the provided handler and callbacks. + HandleTask( + this._handler, + this.identifier, + Emittable _emittable, + this._onStart, + this._onDone, { + @visibleForTesting HandleTaskEmitter? emitter, + }) : _emitter = emitter ?? HandleTaskEmitter(_emittable); + + /// Cancels the task. + /// + /// This will prevent the task from emitting any more state updates + /// and will mark the task as canceled. + void cancel() => _emitter.cancel(); + + /// Executes the handler function and manages its lifecycle. + /// + /// This method: + /// 1. Calls [onBegin] to signal the start of the task + /// 2. Executes the handler function + /// 3. Handles any errors that occur during execution + /// 4. Marks the task as complete + /// 5. Calls [onComplete] to signal the end of the task + Future handle( + void Function() onBegin, + void Function() onComplete, + ) async { + try { + _onBegin(onBegin); + await _handler(_emitter); + } on Object catch (error, stackTrace) { + // Only complete the emitter if it's not already done + if (!_emitter.isDone) { + _emitter.completeError(error, stackTrace); + } + } finally { + // Only complete the emitter if it's not already done + if (!_emitter.isDone) { + _emitter.complete(); + } + _onComplete(onComplete); + } + } + + /// Calls the [onBegin] callback and then the [onStart] callback. + void _onBegin(void Function() onBegin) { + onBegin(); + _onStart(); + } + + /// Calls the [onComplete] callback and then the [onDone] callback. + void _onComplete(void Function() onComplete) { + onComplete(); + _onDone(); + } +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state/lib/src/function_stream_handler/handle_task_emitter.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state/lib/src/function_stream_handler/handle_task_emitter.dart new file mode 100644 index 0000000..7f671e7 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state/lib/src/function_stream_handler/handle_task_emitter.dart @@ -0,0 +1,99 @@ +import 'dart:async'; + +import 'package:meta/meta.dart'; + +import '../base/interface.dart'; + +/// Internal implementation of [Emitter] used by [HandleTaskImpl]. +/// +/// This class manages the state of a single emit operation and provides +/// methods for completion and cancellation. +@internal +class HandleTaskEmitter implements Emitter { + /// The function to emit the state. + final void Function(State state) _emit; + + /// The completer for the emitter. + final _completer = Completer(); + + /// Whether the emitter has been canceled. + var _isCanceled = false; + + /// Whether the emitter has been completed. + var _isCompleted = false; + + /// Whether the emitter is done. + @override + bool get isDone => _isCanceled || _isCompleted; + + /// Returns true if the emitter has been canceled. + bool get isCanceled => _isCanceled; + + /// A future that completes when the emitter is done. + Future get future => _completer.future; + + /// Creates a new [HandleTaskEmitter] with the provided emit function. + HandleTaskEmitter(this._emit); + + @override + void call(State state) { + assert( + !_isCompleted, + 'The emitter has already been completed. ' + 'This usually happens because of an unawaited future in your handler. ' + 'Make sure to await all asynchronous operations and check emit.isDone ' + 'before emitting to avoid this issue.', + ); + + // Only emit if the emitter is not canceled + if (!_isCanceled) { + _emit(state); + } + } + + /// Cancels the emitter. + /// + /// After calling this method, the emitter will no longer accept new state + /// updates and the [future] will complete normally. + void cancel() { + if (isDone) { + return; + } + + _isCanceled = true; + if (!_completer.isCompleted) { + _completer.complete(); + } + } + + /// Marks the emitter as complete. + /// + /// After calling this method, the emitter will no longer accept new state + /// updates and the [future] will complete normally. + void complete() { + if (isDone) { + return; + } + + _isCompleted = true; + if (!_completer.isCompleted) { + _completer.complete(); + } + } + + /// Completes the emitter with an error. + /// + /// After calling this method, the emitter will no longer accept new state + /// updates and the [future] will complete with the provided [error] and + /// [stackTrace]. + void completeError(Object error, StackTrace stackTrace) { + if (isDone) { + return; + } + + _isCompleted = true; + if (!_completer.isCompleted) { + _completer.completeError(error, stackTrace); + } + } +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state/lib/src/function_stream_handler/stream_function_handler.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state/lib/src/function_stream_handler/stream_function_handler.dart new file mode 100644 index 0000000..f9303bc --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state/lib/src/function_stream_handler/stream_function_handler.dart @@ -0,0 +1,136 @@ +import 'dart:async'; + +import 'package:meta/meta.dart'; + +import '../base/interface.dart'; +import 'handle_task.dart'; + +/// A function handler implementation that uses streams to process state updates. +/// +/// This class provides the core implementation for different function handling +/// strategies through the use of transformers. +class StreamFunctionHandler + implements FunctionHandler { + /// The transformer that determines how tasks are processed. + final HandleTaskTransformer _handleTransformer; + + /// The controller for the task stream. + final _taskController = StreamController>.broadcast(); + + /// The list of tasks. + final _tasks = >[]; + + /// The subscription for the task stream. + late final StreamSubscription> _taskSub; + + /// Whether the handler is closing. + bool _isClosing = false; + + /// Whether the handler is closed. + @override + bool get isClosed => _taskController.isClosed; + + /// Creates a new [StreamFunctionHandler] with the provided [handleTransformer]. + /// + /// The [handleTransformer] determines how tasks are processed (sequentially, + /// concurrently, etc.). + StreamFunctionHandler({ + required HandleTaskTransformer handleTransformer, + }) : _handleTransformer = handleTransformer { + _taskSub = _onTask(); + } + + @override + Future call( + EmitterHandler handler, + Object? identifier, { + required Emittable onEmit, + required void Function(Object error, StackTrace stackTrace) onError, + required void Function() onStart, + required void Function() onDone, + }) async { + if (isClosed) { + throw StateError('Cannot handle new handler after calling close'); + } + + final task = HandleTask( + handler, + identifier, + onEmit, + onStart, + onDone, + ); + + _taskController.add(task); + try { + await task.future; + } on Object catch (error, stackTrace) { + onError(error, stackTrace); + } + } + + /// Closes the function handler and cancels all active tasks. + /// + /// This method is idempotent - calling it multiple times has the same effect + /// as calling it once. + @mustCallSuper + @override + Future close() async { + if (isClosed || _isClosing) { + return; + } + + _isClosing = true; + + try { + // Close the task controller + await _taskController.close(); + + // Cancel all active tasks first + for (final task in List>.from(_tasks)) { + task.cancel(); + } + + // Wait for all tasks to complete + // + // We ignore the error, because it is expected that + // it might throw an error. + await Future.wait(_tasks.map((e) => e.future)).onError( + (_, __) => const [], + ); + + // Cancel the subscription + await _taskSub.cancel(); + } finally { + _isClosing = false; + } + } + + /// Sets up the task processing pipeline using the provided transformer. + StreamSubscription> _onTask() { + final subscription = _handleTransformer( + _taskController.stream, + (task) { + final controller = StreamController>.broadcast( + sync: true, + onCancel: task.cancel, + ); + + // Add the task to the list of tasks + void onBegin() => _tasks.add(task); + + // Remove the task from the list of tasks + void onComplete() { + _tasks.remove(task); + if (!controller.isClosed) { + controller.close(); + } + } + + task.handle(onBegin, onComplete); + return controller.stream; + }, + ).listen(null); + return subscription; + } +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state/lib/src/mixin/state_manager_listener_mixin.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state/lib/src/mixin/state_manager_listener_mixin.dart new file mode 100644 index 0000000..d415518 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state/lib/src/mixin/state_manager_listener_mixin.dart @@ -0,0 +1,73 @@ +import 'package:meta/meta.dart'; + +import '../base/interface.dart'; +import '../base/state_manager_base.dart'; +import '../state_manager_observer.dart'; +import '../state_manager_overrides.dart'; + +/// A mixin that provides default lifecycle event handling for state managers. +/// +/// This mixin implements the [StateManagerListener] interface and forwards +/// all lifecycle events to the global [StateManagerObserver] configured in +/// [StateManagerOverrides]. +/// +/// The mixin is typically used with [StateManagerBase] to provide standard +/// event observation capabilities with minimal boilerplate. +@internal +mixin StateManagerListenerMixin + on StateManagerBase implements StateManagerListener { + /// The observer that will receive lifecycle events from this state manager. + /// + /// By default, this returns the global observer from [StateManagerOverrides]. + StateManagerObserver get _observer => StateManagerOverrides.observer; + + /// Called when the state manager is created. + /// + /// Forwards the creation event to the observer. + @mustCallSuper + @protected + @override + void onCreate() => _observer.onCreate(this); + + /// Called when a handler function starts execution. + /// + /// Forwards the handler start event to the observer with the optional identifier. + @mustCallSuper + @protected + @override + void onStart(Object? identifier) => _observer.onHandleStart(this, identifier); + + /// Called when a handler function completes execution. + /// + /// Forwards the handler completion event to the observer with the optional identifier. + @mustCallSuper + @protected + @override + void onDone(Object? identifier) => _observer.onHandleDone(this, identifier); + + /// Called when the state changes. + /// + /// Forwards the state change event to the observer with the current and next state. + @mustCallSuper + @protected + @override + void onChange(State currentState, State nextState, Object? identifier) => + _observer.onChange(this, currentState, nextState, identifier); + + /// Called when an error occurs within the state manager. + /// + /// Forwards the error event to the observer with the error and stack trace. + @mustCallSuper + @protected + @override + void onError(Object error, StackTrace stackTrace, Object? identifier) => + _observer.onError(this, error, stackTrace, identifier); + + /// Called when the state manager is closed. + /// + /// Forwards the close event to the observer. + @mustCallSuper + @protected + @override + void onClose() => _observer.onClose(this); +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state/lib/src/state_manager.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state/lib/src/state_manager.dart new file mode 100644 index 0000000..d4cc92b --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state/lib/src/state_manager.dart @@ -0,0 +1,51 @@ +import 'base/interface.dart'; +import 'base/state_manager_base.dart'; +import 'mixin/state_manager_listener_mixin.dart'; +import 'state_manager_overrides.dart'; + +/// Main class for state management that should be extended by users. +/// +/// This class combines the core state management functionality from +/// [StateManagerBase] with the default listener implementation from +/// [StateManagerListenerMixin]. +/// +/// Usage example: +/// ```dart +/// class CounterState { +/// final int count; +/// +/// CounterState(this.count); +/// } +/// +/// class CounterManager extends StateManager { +/// CounterManager() : super(CounterState(0)); +/// +/// Future increment() => handle((emit) async { +/// emit(CounterState(state.count + 1)); +/// }); +/// +/// Future decrement() => handle((emit) async { +/// emit(CounterState(state.count - 1)); +/// }); +/// } +/// ``` +abstract class StateManager + extends StateManagerBase with StateManagerListenerMixin { + /// Creates a new [StateManager] with the provided initial [state] + /// and optional function [handler]. + /// + /// If no [handler] is provided, the default handler from [StateManagerOverrides.defaultHandlerFactory] + /// will be used, which is a sequential handler by default. + StateManager( + super.state, { + FunctionHandler? handler, + }) : super(handler: handler ?? _createDefaultHandler()); + + /// Creates a default handler for the given state type. + /// + /// This method is used to create a default handler for the given state type. + /// It uses the [StateManagerOverrides.defaultHandlerFactory] to create a new handler. + static FunctionHandler _createDefaultHandler() { + return StateManagerOverrides.defaultHandlerFactory(); + } +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state/lib/src/state_manager_observer.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state/lib/src/state_manager_observer.dart new file mode 100644 index 0000000..5006b9d --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state/lib/src/state_manager_observer.dart @@ -0,0 +1,110 @@ +import 'package:meta/meta.dart'; + +import 'base/state_manager_base.dart'; + +/// An abstract class for observing state manager lifecycle events. +/// +/// The observer pattern allows you to monitor and respond to various events +/// in the state management lifecycle across your application. You can extend +/// this class to create custom observers for logging, analytics, debugging, +/// or any other cross-cutting concern. +/// +/// To use a custom observer, assign it to [StateManagerOverrides.observer]: +/// ```dart +/// class MyCustomObserver extends StateManagerObserver { +/// @override +/// void onChange( +/// StateManagerBase stateManager, +/// Object? currentState, +/// Object? nextState, +/// Object? identifier, +/// ) { +/// print('State changed from $currentState to $nextState with identifier: $identifier'); +/// super.onChange(stateManager, currentState, nextState, identifier); +/// } +/// } +/// +/// // Set the observer globally +/// StateManagerOverrides.observer = MyCustomObserver(); +/// ``` +abstract class StateManagerObserver { + /// Creates a new [StateManagerObserver] instance. + const StateManagerObserver(); + + /// Called when a state manager is created. + /// + /// This is the first lifecycle method called, right after the state manager + /// is instantiated with its initial state. + /// + /// [stateManager] - The state manager that was created + @mustCallSuper + void onCreate(StateManagerBase stateManager) {} + + /// Called when a state change occurs. + /// + /// This method is invoked whenever a new state is emitted by the state manager, + /// providing both the previous state and the new state. + /// + /// [stateManager] - The state manager where the change occurred + /// [currentState] - The previous state value + /// [nextState] - The new state value + /// [identifier] - An optional identifier for the handler + @mustCallSuper + void onChange( + StateManagerBase stateManager, + Object? currentState, + Object? nextState, + Object? identifier, + ) {} + + /// Called when an error occurs within a state manager. + /// + /// This method is invoked whenever an exception is caught during state + /// management operations. + /// + /// [stateManager] - The state manager where the error occurred + /// [error] - The error that was thrown + /// [stackTrace] - The stack trace associated with the error + /// [identifier] - An optional identifier for the handler + @mustCallSuper + void onError( + StateManagerBase stateManager, + Object error, + StackTrace stackTrace, + Object? identifier, + ) {} + + /// Called when a handler function starts execution. + /// + /// This method is invoked at the beginning of each handler function's lifecycle. + /// + /// [stateManager] - The state manager handling the function + /// [identifier] - An optional identifier for the handler function + @mustCallSuper + void onHandleStart( + StateManagerBase stateManager, + Object? identifier, + ) {} + + /// Called when a handler function completes execution. + /// + /// This method is invoked after a handler function has completed, + /// regardless of whether it was successful or threw an error. + /// + /// [stateManager] - The state manager handling the function + /// [identifier] - An optional identifier for the handler function + @mustCallSuper + void onHandleDone( + StateManagerBase stateManager, + Object? identifier, + ) {} + + /// Called when a state manager is closed. + /// + /// This is the final lifecycle method called when a state manager + /// is being disposed and will no longer emit state updates. + /// + /// [stateManager] - The state manager that was closed + @mustCallSuper + void onClose(StateManagerBase stateManager) {} +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state/lib/src/state_manager_overrides.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state/lib/src/state_manager_overrides.dart new file mode 100644 index 0000000..f292a8b --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state/lib/src/state_manager_overrides.dart @@ -0,0 +1,99 @@ +import 'package:yx_state/yx_state.dart'; + +/// A factory function that creates a [FunctionHandler] with a specific state type. +typedef FunctionHandlerFactory = FunctionHandler + Function(); + +/// Global configuration for state managers. +/// +/// This class provides a way to override default behaviors across all state managers +/// in your application. It defines global settings like the default observer and +/// function handler factory used by state managers. +/// +/// You can customize these settings to apply consistent behavior across your +/// entire application without having to configure each state manager individually. +/// +/// Example: +/// ```dart +/// // Set a custom observer for all state managers +/// StateManagerOverrides.observer = MyCustomObserver(); +/// ``` +abstract class StateManagerOverrides { + /// The default [StateManagerObserver] implementation. + /// + /// This observer provides basic logging and error handling for state manager events. + /// It can be overridden to provide custom observer behavior. + /// + /// Example: + /// ```dart + /// class MyObserver extends StateManagerObserver { + /// @override + /// void onCreate(StateManagerBase manager) { + /// print('${manager.runtimeType} created with initial state: ${manager.state}'); + /// super.onCreate(manager); + /// } + /// } + /// + /// // Set the custom observer + /// StateManagerOverrides.observer = MyObserver(); + /// ``` + static StateManagerObserver observer = const _DefaultStateManagerObserver(); + + /// Factory function that creates the default [FunctionHandler] for state managers. + /// + /// By default, this returns a sequential function handler, which processes tasks + /// one at a time in a queue. This can be changed to use a different handler type + /// globally across the application. + /// + /// See also: + /// + /// * [package:yx_state_transformers](https://pub.dev/packages/yx_state_transformers) + static FunctionHandlerFactory defaultHandlerFactory = + () => StreamFunctionHandler( + handleTransformer: (tasks, mapper) => tasks.asyncExpand(mapper)); + + /// The default shouldEmit implementation. + /// + /// This implementation compares the current state and the next state using the + /// `!=` operator. + /// + /// This can be overridden to provide a custom shouldEmit implementation. + /// + /// Example: + /// ```dart + /// StateManagerOverrides.defaultShouldEmit = (current, next) => true; + /// ``` + static bool Function(Object? current, Object? next) defaultShouldEmit = + (current, next) => current != next; +} + +/// The default implementation of [StateManagerObserver]. +/// +/// This observer provides a minimal implementation that doesn't perform any actions +/// when state manager events occur. It serves as a no-op placeholder until a custom +/// observer is provided. +/// +/// This allows the state management system to have a non-null observer by default, +/// which simplifies the internal code by avoiding null checks. +/// +/// To add custom observation behavior, create your own observer class and set it +/// as the global observer: +/// ```dart +/// class LoggingObserver extends StateManagerObserver { +/// @override +/// void onChange( +/// StateManagerBase stateManager, +/// Object? currentState, +/// Object? nextState, +/// Object? identifier, +/// ) { +/// print('${stateManager.runtimeType}: $currentState -> $nextState (identifier: $identifier)'); +/// super.onChange(stateManager, currentState, nextState, identifier); +/// } +/// } +/// +/// StateManagerOverrides.observer = LoggingObserver(); +/// ``` +class _DefaultStateManagerObserver extends StateManagerObserver { + const _DefaultStateManagerObserver(); +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state/lib/src/test_util/state_manager_base_test_util.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state/lib/src/test_util/state_manager_base_test_util.dart new file mode 100644 index 0000000..fa1cec6 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state/lib/src/test_util/state_manager_base_test_util.dart @@ -0,0 +1,63 @@ +part of '../base/state_manager_base.dart'; + +/// {@template state_manager_base_test_util} +/// A utility class for testing [StateManagerBase] instances. +/// +/// This class provides a method to set the state of the state manager without +/// going through the normal handler flow. +/// +/// {@tool snippet} +/// ```dart +/// import 'package:test/test.dart'; +/// import 'package:yx_state/yx_state.dart'; +/// +/// class TestStateManager extends StateManager { +/// TestStateManager() : super(0); +/// } +/// +/// void main() { +/// late TestStateManager stateManager; +/// late StateManagerBaseTestUtil testUtil; +/// +/// setUp(() { +/// stateManager = TestStateManager(); +/// testUtil = StateManagerBaseTestUtil(stateManager); +/// }); +/// +/// tearDown(() => stateManager.close()); +/// +/// test('util usage example', () { +/// // arrange +/// const expectedState = 1; +/// +/// // act +/// testUtil.emit(expectedState); +/// +/// // assert +/// expect(stateManager.state, expectedState); +/// }); +/// } +/// ``` +/// {@end-tool} +/// {@endtemplate} +@visibleForTesting +class StateManagerBaseTestUtil { + final StateManagerBase _stateManager; + + /// The identifier for the test util. + String get identifier => _identifier; + + /// {@macro state_manager_base_test_util} + const StateManagerBaseTestUtil(this._stateManager); + + /// Base constant identifier for the test util. + static const String _identifier = 'test_util'; + + /// Sets the state to the provided value without going through the normal + /// handler flow. + /// + /// This method is primarily intended for testing purposes. It is not + /// recommended to use this method in production code. + @visibleForTesting + void emit(State state) => _stateManager._emit(state, identifier); +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state/lib/yx_state.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state/lib/yx_state.dart new file mode 100644 index 0000000..9e3d6c3 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state/lib/yx_state.dart @@ -0,0 +1,11 @@ +/// A state management library for [Dart](https://dart.dev)/[Flutter](https://flutter.dev) applications. +library yx_state; + +export 'src/base/interface.dart' + hide StateManagerHandler, StateManagerListener, Closable; +export 'src/base/state_manager_base.dart'; +export 'src/function_stream_handler/handle_task.dart'; +export 'src/function_stream_handler/stream_function_handler.dart'; +export 'src/state_manager.dart'; +export 'src/state_manager_observer.dart'; +export 'src/state_manager_overrides.dart'; diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state/pubspec.yaml b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state/pubspec.yaml new file mode 100644 index 0000000..2998ea5 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state/pubspec.yaml @@ -0,0 +1,22 @@ +name: yx_state +description: A state management library for Dart/Flutter applications. +version: 1.0.0 +repository: https://github.com/yandex/city-services-pub/tree/main/yx_state/packages/yx_state +issue_tracker: https://github.com/yandex/city-services-pub/issues +topics: + - yx-state + - state-management + +environment: + sdk: ">=2.19.0 <4.0.0" + +dependencies: + meta: ^1.3.0 + +dev_dependencies: + test: ^1.18.2 + mocktail: ^1.0.0 + +screenshots: + - description: The yx_state package logo + path: screenshots/yx_state_logo.webp diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state/screenshots/yx_state_logo.webp b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state/screenshots/yx_state_logo.webp new file mode 100644 index 0000000..84fa3c5 Binary files /dev/null and b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state/screenshots/yx_state_logo.webp differ diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state/test/src/function_stream_handler/handle_task_emitter_test.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state/test/src/function_stream_handler/handle_task_emitter_test.dart new file mode 100644 index 0000000..f4a7f85 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state/test/src/function_stream_handler/handle_task_emitter_test.dart @@ -0,0 +1,125 @@ +import 'package:mocktail/mocktail.dart'; +import 'package:test/test.dart'; +import 'package:yx_state/src/function_stream_handler/handle_task_emitter.dart'; + +import '../mocks.dart'; + +void main() { + group('HandleTaskEmitter', () { + late MockEmitter mockEmitter; + late HandleTaskEmitter handleTaskEmitter; + + setUp(() { + mockEmitter = MockEmitter(); + handleTaskEmitter = HandleTaskEmitter(mockEmitter.call); + }); + + group('method call', () { + test('should throw assert if the emitter is completed', () { + // arrange + handleTaskEmitter.complete(); + + // act & assert + expect(() => handleTaskEmitter.call(1), throwsA(isA())); + }); + + test('should not emit if the emitter is canceled', () { + // arrange + handleTaskEmitter.cancel(); + + // act + handleTaskEmitter.call(1); + + // assert + verifyNever(() => mockEmitter.call(any())); + }); + + test('should emit if the emitter is not canceled', () { + // arrange + const expected = 1; + + // act + handleTaskEmitter.call(1); + + // assert + verify(() => mockEmitter.call(expected)).called(1); + }); + }); + + group('method cancel', () { + test('should cancel the emitter', () { + // assert + expect(handleTaskEmitter.isCanceled, isFalse); + + // act + handleTaskEmitter.cancel(); + + // assert + expect(handleTaskEmitter.isCanceled, isTrue); + }); + + test('should set isDone to true', () { + // assert + expect(handleTaskEmitter.isDone, isFalse); + + // act + handleTaskEmitter.cancel(); + + // assert + expect(handleTaskEmitter.isDone, isTrue); + }); + + test('should complete the future', () { + // assert + expectLater(handleTaskEmitter.future, completes); + + // act + handleTaskEmitter.cancel(); + }); + }); + + group('method complete', () { + test('should set isDone to true', () { + // assert + expect(handleTaskEmitter.isDone, isFalse); + + // act + handleTaskEmitter.complete(); + + // assert + expect(handleTaskEmitter.isDone, isTrue); + }); + + test('should complete the future', () { + // assert + expectLater(handleTaskEmitter.future, completes); + + // act + handleTaskEmitter.complete(); + }); + }); + + group('method completeError', () { + test('should complete the future with an error', () { + // assert + expectLater(handleTaskEmitter.future, throwsException); + + // act + handleTaskEmitter.completeError(Exception(), StackTrace.current); + }); + + test('should set isDone to true', () { + // assert + expect(handleTaskEmitter.isDone, isFalse); + // need because completer throw exception to zone + expect(handleTaskEmitter.future, throwsException); + + // act + handleTaskEmitter.completeError(Exception(), StackTrace.current); + + // assert + expect(handleTaskEmitter.isDone, isTrue); + }); + }); + }); +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state/test/src/function_stream_handler/handle_task_test.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state/test/src/function_stream_handler/handle_task_test.dart new file mode 100644 index 0000000..9bd7454 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state/test/src/function_stream_handler/handle_task_test.dart @@ -0,0 +1,205 @@ +import 'package:mocktail/mocktail.dart'; +import 'package:test/test.dart'; +import 'package:yx_state/yx_state.dart'; + +import '../mocks.dart'; + +class TestHandlerHelper { + Object? identifier; + + bool isStartCalled = false; + void onStart() { + isStartCalled = true; + } + + bool isDoneCalled = false; + void onDone() { + isDoneCalled = true; + } + + Future Function(Emitter emit)? onHandleCallback; + Future handle(Emitter emit) async { + await onHandleCallback?.call(emit); + } + + // ignore: unused_element + void onEmit(State state) {} +} + +void main() { + group('HandleTaskImpl', () { + late TestHandlerHelper testHandlerHelper; + late MockHandleTaskEmitter handleTaskEmitter; + late HandleTask handleTask; + + setUp(() { + testHandlerHelper = TestHandlerHelper(); + handleTaskEmitter = MockHandleTaskEmitter(); + handleTask = HandleTask( + testHandlerHelper.handle, + testHandlerHelper.identifier, + testHandlerHelper.onEmit, + testHandlerHelper.onStart, + testHandlerHelper.onDone, + emitter: handleTaskEmitter, + ); + + registerFallbackValue(StackTrace.empty); + }); + + group('method cancel', () { + test('should call the emitter', () { + // arrange + when(() => handleTaskEmitter.cancel()) + .thenAnswer((_) => Future.value()); + + // act + handleTask.cancel(); + + // assert + verify(() => handleTaskEmitter.cancel()).called(1); + }); + }); + + group('method handle', () { + setUp(() { + when(() => handleTask.isDone).thenReturn(false); + }); + + test('should call onBegin when the handle is called', () async { + // arrange + bool isBeginCalled = false; + + // act + await handleTask.handle(() => isBeginCalled = true, () {}); + + // assert + expect(isBeginCalled, isTrue); + }); + + test('should call onStart when the handle is called', () async { + // arrange + expect(testHandlerHelper.isStartCalled, isFalse); + + // act + await handleTask.handle(() {}, () {}); + + // assert + expect(testHandlerHelper.isStartCalled, isTrue); + }); + + test('should call handle when the handle is called', () async { + // arrange + bool isHandleCalled = false; + testHandlerHelper.onHandleCallback = (emit) async { + isHandleCalled = true; + }; + + // act + await handleTask.handle(() {}, () {}); + + // assert + expect(isHandleCalled, isTrue); + }); + + test('should emit the state when the handle is called', () async { + // arrange + final expected = 1; + testHandlerHelper.onHandleCallback = (emit) async { + emit(expected); + }; + + // act + await handleTask.handle(() {}, () {}); + + // assert + verify(() => handleTaskEmitter.call(expected)).called(1); + }); + + test('should call completeError if the handle throws an error', () async { + // arrange + testHandlerHelper.onHandleCallback = (emit) async { + throw Exception(); + }; + + // act + await handleTask.handle(() {}, () {}); + + // assert + verify( + () => handleTaskEmitter.completeError( + isA(), + any(), + ), + ).called(1); + }); + + test('should not call completeError if emitter isNotDone', () async { + // arrange + testHandlerHelper.onHandleCallback = (emit) async { + when(() => handleTaskEmitter.isDone).thenReturn(true); + throw Exception(); + }; + + // act + await handleTask.handle(() {}, () {}); + + // assert + verifyNever( + () => handleTaskEmitter.completeError( + isA(), + any(), + ), + ); + }); + + test('should call complete after the handler has completed', () async { + // arrange + testHandlerHelper.onHandleCallback = (emit) async { + await Future.delayed(const Duration(milliseconds: 100)); + }; + + // act + await handleTask.handle(() {}, () {}); + + // assert + verify(() => handleTaskEmitter.complete()).called(1); + }); + + test('should not call complete if emitter isDone', () async { + // arrange + testHandlerHelper.onHandleCallback = (emit) async { + when(() => handleTaskEmitter.isDone).thenReturn(true); + }; + + // act + await handleTask.handle(() {}, () {}); + + // assert + verifyNever(() => handleTaskEmitter.complete()); + }); + + test('should call onComplete after the handler has completed', () async { + // arrange + bool isCompleteCalled = false; + + // act + await handleTask.handle(() {}, () => isCompleteCalled = true); + + // assert + expect(isCompleteCalled, isTrue); + }); + + test('should call onDone after the handler has completed', () async { + // arrange + expect(testHandlerHelper.isDoneCalled, isFalse); + + // act + await handleTask.handle(() {}, () {}); + + // assert + expect(testHandlerHelper.isDoneCalled, isTrue); + }); + }); + }); +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state/test/src/function_stream_handler/stream_function_handler_test.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state/test/src/function_stream_handler/stream_function_handler_test.dart new file mode 100644 index 0000000..c0d65ea --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state/test/src/function_stream_handler/stream_function_handler_test.dart @@ -0,0 +1,172 @@ +import 'dart:async'; + +import 'package:test/test.dart'; +import 'package:yx_state/yx_state.dart'; + +// Define a HandleTaskTransformer function +Stream> testTransformer( + Stream> tasks, + HandleTaskMapper mapper, +) => + tasks.asyncExpand(mapper); + +void main() { + group('StreamFunctionHandler', () { + late StreamFunctionHandler handler; + + setUp(() { + handler = StreamFunctionHandler( + handleTransformer: testTransformer, + ); + }); + + tearDown(() async { + // Clean up resources + await handler.close(); + }); + + group('getter isClosed', () { + test('should return false when the handler is not closed', () { + // assert + expect(handler.isClosed, isFalse); + }); + + test('should return true when the handler is closed', () async { + // act + await handler.close(); + + // assert + expect(handler.isClosed, isTrue); + }); + }); + + group('method close', () { + test('should be idempotent', () async { + // arrange + await handler.close(); + + // act + await handler.close(); // Second call should not throw + + // assert + expect(handler.isClosed, isTrue); + }); + }); + + group('method call', () { + test('should throw StateError when calling after closed', () async { + // arrange + await handler.close(); + + // act & assert + expect( + () => handler.call( + (emit) async {}, + null, + onEmit: (_) {}, + onError: (_, __) {}, + onStart: () {}, + onDone: () {}, + ), + throwsStateError, + ); + }); + + test('should handle errors from the task', () async { + // arrange + final error = Exception('Test error'); + bool errorCalled = false; + + // act & assert + await handler.call( + (emit) => throw error, + null, + onEmit: (_) {}, + onError: (error, _) { + expect(error, equals(error)); + errorCalled = true; + }, + onStart: () {}, + onDone: () {}, + ); + + // assert + expect(errorCalled, isTrue); + }); + + test('should call onEmit for each task', () async { + // arrange + final expectedValue = 0; + + // act & assert + await handler.call( + (emit) async => emit(expectedValue), + null, + onEmit: (value) => expect(value, equals(expectedValue)), + onError: (_, __) {}, + onStart: () {}, + onDone: () {}, + ); + }); + + test('should emit the correct value for each task', () async { + // act & assert + for (var i = 0; i < 3; i++) { + handler.call( + (emit) async { + // Just emit something + emit(i); + }, + 'test_$i', + onEmit: (value) => expect(value, equals(i)), + onError: (_, __) {}, + onStart: () {}, + onDone: () {}, + ); + } + }); + + test('should complete all future tasks after close', () async { + // act & assert + for (var i = 0; i < 3; i++) { + expectLater( + handler.call( + (emit) async {}, + null, + onEmit: (value) {}, + onError: (_, __) {}, + onStart: () {}, + onDone: () {}, + ), + completes, + ); + } + + // act + await handler.close(); + }); + + test('should complete all future tasks with error', () async { + // arrange & act & assert + for (var i = 0; i < 3; i++) { + final error = Exception('Test error $i'); + + expectLater( + handler.call( + (emit) async => throw error, + null, + onEmit: (value) {}, + onError: (error, _) => expect(error, equals(error)), + onStart: () {}, + onDone: () {}, + ), + completes, // completes with out error + ); + } + + // act + await handler.close(); + }); + }); + }); +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state/test/src/mixin/state_manager_listener_mixin_test.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state/test/src/mixin/state_manager_listener_mixin_test.dart new file mode 100644 index 0000000..f6c02c6 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state/test/src/mixin/state_manager_listener_mixin_test.dart @@ -0,0 +1,139 @@ +import 'package:mocktail/mocktail.dart'; +import 'package:test/test.dart'; +import 'package:yx_state/src/mixin/state_manager_listener_mixin.dart'; +import 'package:yx_state/yx_state.dart'; + +import '../mocks.dart'; + +class TestStateManager extends StateManagerBase + with StateManagerListenerMixin { + TestStateManager(super.state, {required super.handler}); + + @override + void onCreate() { + super.onCreate(); + } + + @override + void onStart(Object? identifier) { + super.onStart(identifier); + } + + @override + void onDone(Object? identifier) { + super.onDone(identifier); + } + + @override + void onChange(int current, int next, Object? identifier) { + super.onChange(current, next, identifier); + } + + @override + void onError(Object error, StackTrace stackTrace, Object? identifier) { + super.onError(error, stackTrace, identifier); + } + + @override + void onClose() { + super.onClose(); + } +} + +void main() { + group('StateManagerListenerMixin', () { + late StateManagerObserver originalObserver; + + late MockStateManagerObserver observer; + late MockFunctionHandler handler; + late TestStateManager stateManager; + + setUp(() { + observer = MockStateManagerObserver(); + + originalObserver = StateManagerOverrides.observer; + StateManagerOverrides.observer = observer; + + handler = MockFunctionHandler(); + stateManager = TestStateManager(0, handler: handler); + }); + + tearDown(() async { + await stateManager.close(); + reset(observer); + StateManagerOverrides.observer = originalObserver; + }); + + test('should call onCreate', () { + // assert + // 1 time because constructor already calls it + verify(() => observer.onCreate(stateManager)).called(1); + + // act + stateManager.onCreate(); + + // assert + verify(() => observer.onCreate(stateManager)).called(1); + }); + + test('should call onStart', () { + // arrange + const identifier = 'test'; + + // act + stateManager.onStart(identifier); + + // assert + verify(() => observer.onHandleStart(stateManager, identifier)).called(1); + }); + + test('should call onDone', () { + // arrange + const identifier = 'test'; + + // act + stateManager.onDone(identifier); + + // assert + verify(() => observer.onHandleDone(stateManager, identifier)).called(1); + }); + + test('should call onChange', () { + // arrange + const current = 1; + const next = 2; + const identifier = 'test'; + + // act + stateManager.onChange(current, next, identifier); + + // assert + verify( + () => observer.onChange(stateManager, current, next, identifier), + ).called(1); + }); + + test('should call onError', () { + // arrange + final error = Exception(); + final stackTrace = StackTrace.current; + const identifier = 'test'; + + // act + stateManager.onError(error, stackTrace, identifier); + + // assert + verify( + () => observer.onError(stateManager, error, stackTrace, identifier), + ).called(1); + }); + + test('should call onClose', () { + // act + stateManager.onClose(); + + // assert + verify(() => observer.onClose(stateManager)).called(1); + }); + }); +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state/test/src/mocks.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state/test/src/mocks.dart new file mode 100644 index 0000000..cd49c3f --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state/test/src/mocks.dart @@ -0,0 +1,14 @@ +import 'package:mocktail/mocktail.dart'; +import 'package:yx_state/src/function_stream_handler/handle_task_emitter.dart'; +import 'package:yx_state/yx_state.dart'; + +class MockEmitter extends Mock + implements Emitter {} + +class MockHandleTaskEmitter extends Mock + implements HandleTaskEmitter {} + +class MockStateManagerObserver extends Mock implements StateManagerObserver {} + +class MockFunctionHandler extends Mock + implements FunctionHandler {} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state/test/src/state_manager_test.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state/test/src/state_manager_test.dart new file mode 100644 index 0000000..93e98a6 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state/test/src/state_manager_test.dart @@ -0,0 +1,622 @@ +import 'dart:async'; + +import 'package:meta/meta.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:test/test.dart'; +import 'package:yx_state/src/mixin/state_manager_listener_mixin.dart'; +import 'package:yx_state/yx_state.dart'; + +class MockFunctionHandler extends Mock + implements FunctionHandler {} + +class TestStateManager extends StateManager { + TestStateManager(super.state, {super.handler}); + + Function(int current, int next)? shouldEmitOverride; + + @visibleForTesting + @override + bool shouldEmit(int current, int next) { + final override = shouldEmitOverride; + if (override != null) { + return override(current, next); + } + return super.shouldEmit(current, next); + } + + @visibleForTesting + @override + void addError(Object error, [StackTrace? stackTrace, Object? identifier]) { + super.addError(error, stackTrace, identifier); + } + + @visibleForTesting + @override + // ignore: invalid_override_of_non_virtual_member + Future handle(EmitterHandler handler, {Object? identifier}) { + return super.handle(handler, identifier: identifier); + } + + // Track if lifecycle methods are called + bool onCreateCalled = false; + + // Track if onStart is called + bool onStartCalled = false; + Object? startIdentifier; + + // Track if onDone is called + bool onDoneCalled = false; + Object? doneIdentifier; + + // Track if onChange is called + bool onChangeCalled = false; + int? prevState; + int? curState; + Object? onChangeIdentifier; + + // Track if onError is called + bool onErrorCalled = false; + Object? lastError; + StackTrace? lastStackTrace; + + // Track if onClose is called + bool onCloseCalled = false; + + @visibleForTesting + @override + void onCreate() { + onCreateCalled = true; + super.onCreate(); + } + + @visibleForTesting + @override + void onStart(Object? identifier) { + onStartCalled = true; + startIdentifier = identifier; + super.onStart(identifier); + } + + @visibleForTesting + @override + void onDone(Object? identifier) { + onDoneCalled = true; + doneIdentifier = identifier; + super.onDone(identifier); + } + + @visibleForTesting + @override + void onChange(int currentState, int nextState, Object? identifier) { + onChangeCalled = true; + prevState = currentState; + curState = nextState; + onChangeIdentifier = identifier; + super.onChange(currentState, nextState, identifier); + } + + @visibleForTesting + @override + void onError(Object error, StackTrace stackTrace, Object? identifier) { + lastError = error; + lastStackTrace = stackTrace; + onErrorCalled = true; + super.onError(error, stackTrace, identifier); + } + + @visibleForTesting + @override + void onClose() { + onCloseCalled = true; + super.onClose(); + } +} + +class TestFunctionHandler implements FunctionHandler { + @override + bool get isClosed => _isClosed; + + TestFunctionHandler(); + + bool called = false; + bool _isClosed = false; + bool isRethrowError = false; + + @override + Future call( + EmitterHandler handler, + Object? identifier, { + required Emittable onEmit, + required void Function(Object error, StackTrace stackTrace) onError, + required void Function() onStart, + required void Function() onDone, + }) async { + called = true; + + final emitter = TestEmitter(onEmit); + try { + onStart(); + + await handler(emitter); + } on Object catch (error, stackTrace) { + onError(error, stackTrace); + if (isRethrowError) { + rethrow; + } + } finally { + emitter.setIsDone(true); + onDone(); + } + } + + @override + Future close() async => _isClosed = true; +} + +class TestEmitter implements Emitter { + final Function(int state) onEmit; + bool isDoneOverride = false; + TestEmitter(this.onEmit); + + @override + void call(int state) => onEmit(state); + + @override + bool get isDone => isDoneOverride; + + void setIsDone(bool value) => isDoneOverride = value; +} + +void main() { + const initialState = 0; + + group('StateManager', () { + group('constructor', () { + late TestStateManager stateManager; + + // act + setUp(() { + stateManager = TestStateManager(initialState); + }); + + tearDown(() async { + await stateManager.close(); + }); + + test('should call onCreate', () { + // assert + expect(stateManager.onCreateCalled, isTrue); + }); + + test('should initial state be provided', () { + // assert + expect(stateManager.state, initialState); + }); + }); + + group('listener', () { + test('should implement StateManagerListenerMixin', () { + // act + final stateManager = TestStateManager(initialState); + + // assert + expect(stateManager, isA()); + }); + }); + + group('default handler', () { + // need to restore default handler factory after each test + late FunctionHandlerFactory defaultHandlerFactory; + + setUp(() { + defaultHandlerFactory = StateManagerOverrides.defaultHandlerFactory; + }); + + tearDown(() { + StateManagerOverrides.defaultHandlerFactory = defaultHandlerFactory; + }); + + test('default handler from StateManagerOverrides should be used', () { + // arrange + var defaultHandlerCalled = false; + StateManagerOverrides.defaultHandlerFactory = () { + defaultHandlerCalled = true; + return MockFunctionHandler(); + }; + + // act + // ignore: unused_local_variable + final stateManager = TestStateManager(initialState); + + // assert + expect(defaultHandlerCalled, isTrue); + }); + + test( + 'should use updated global handler when defaultHandlerFactory changes', + () async { + // arrange + var firstHandlerCalled = false; + StateManagerOverrides.defaultHandlerFactory = () { + firstHandlerCalled = true; + return MockFunctionHandler(); + }; + + // act + // ignore: unused_local_variable + final stateManager1 = TestStateManager(initialState); + + // assert + expect(firstHandlerCalled, isTrue); + + // arrange + var secondHandlerCalled = false; + StateManagerOverrides.defaultHandlerFactory = () { + secondHandlerCalled = true; + return MockFunctionHandler(); + }; + + // act + // ignore: unused_local_variable + final stateManager2 = TestStateManager(initialState); + + // assert + expect(secondHandlerCalled, isTrue); + }, + ); + + test('should use handler from constructor', () { + // arrange + bool handlerCalled = false; + StateManagerOverrides.defaultHandlerFactory = () { + handlerCalled = true; + return defaultHandlerFactory(); + }; + + // act + // ignore: unused_local_variable + final stateManager = TestStateManager( + initialState, + handler: MockFunctionHandler(), + ); + + // assert + expect(handlerCalled, isFalse); + }); + }); + + group('method handle', () { + late TestFunctionHandler handler; + late TestStateManager stateManager; + + setUp(() { + handler = TestFunctionHandler(); + stateManager = TestStateManager(initialState, handler: handler); + }); + + tearDown(() async { + await stateManager.close(); + }); + + test('should call handler.handle', () async { + // act + await stateManager.handle((_) async {}); + + // assert + expect(handler.called, isTrue); + }); + + test('should call onError if state manager is closed', () async { + // arrange + final stateManager = TestStateManager(initialState, handler: handler); + await stateManager.close(); + + // act + await stateManager.handle((_) async {}); + + // assert + expect(stateManager.onErrorCalled, isTrue); + expect(stateManager.lastError, isA()); + expect(stateManager.lastStackTrace, isNotNull); + }); + + test('should call onStart', () async { + // act + await stateManager.handle((_) async {}); + + // assert + expect(stateManager.onStartCalled, isTrue); + expect(stateManager.startIdentifier, isNull); + }); + + test('should call onStart with identifier', () async { + // arrange + const identifier = 'test'; + + // act + await stateManager.handle( + (_) async {}, + identifier: identifier, + ); + + // assert + expect(stateManager.onStartCalled, isTrue); + expect(stateManager.startIdentifier, equals(identifier)); + }); + + test('should emit state', () async { + // arrange + const expectedState = 1; + + // act + await stateManager.handle((emit) async { + emit(expectedState); + }); + + // assert + expect(stateManager.state, equals(expectedState)); + }); + + test('should emit multiple times', () async { + // arrange + const expectedState1 = 1; + const expectedState2 = 2; + + // act + await stateManager.handle((emit) async { + emit(expectedState1); + await Future.delayed(const Duration(milliseconds: 100)); + emit(expectedState2); + }); + + // assert + expect(stateManager.state, equals(expectedState2)); + }); + + test('should call onError if handler throws error', () async { + // arrange + final error = Exception('Test error'); + + await stateManager.handle((_) async => throw error); + + // assert + expect(stateManager.onErrorCalled, isTrue); + expect(stateManager.lastError, equals(error)); + expect(stateManager.lastStackTrace, isNotNull); + }); + + test('should call onDone', () async { + // act + await stateManager.handle((_) async {}); + + // assert + expect(stateManager.onDoneCalled, isTrue); + }); + + test('should call onDone with identifier', () async { + // arrange + const identifier = 'test'; + + // act + await stateManager.handle( + (_) async {}, + identifier: identifier, + ); + + // assert + expect(stateManager.onDoneCalled, isTrue); + expect(stateManager.doneIdentifier, equals(identifier)); + }); + }); + + group('method addError', () { + test('should call onError', () { + // arrange + final stateManager = TestStateManager(initialState); + final error = Exception('Test error'); + final stackTrace = StackTrace.current; + + // act + stateManager.addError(error, stackTrace); + + // assert + expect(stateManager.onErrorCalled, isTrue); + expect(stateManager.lastError, equals(error)); + expect(stateManager.lastStackTrace, equals(stackTrace)); + }); + }); + + group('method _emit', () { + late TestStateManager stateManager; + late StateManagerBaseTestUtil testUtil; + + setUp(() { + stateManager = TestStateManager(initialState); + testUtil = StateManagerBaseTestUtil(stateManager); + }); + + tearDown(() => stateManager.close()); + + test('should emit state', () { + // arrange + const expectedState = 1; + + // act + testUtil.emit(expectedState); + + // assert + expect(stateManager.state, equals(expectedState)); + }); + + test('should call onChange', () { + // arrange + const expectedState = 1; + + // act + testUtil.emit(expectedState); + + // assert + expect(stateManager.onChangeCalled, isTrue); + expect(stateManager.prevState, equals(initialState)); + expect(stateManager.curState, equals(expectedState)); + expect(stateManager.onChangeIdentifier, testUtil.identifier); + }); + + test('should throw StateError if state manager is closed', () async { + // arrange + final stateManager = TestStateManager(initialState); + final testUtil = StateManagerBaseTestUtil(stateManager); + + // act + await stateManager.close(); + + // assert + expect(() => testUtil.emit(1), throwsStateError); + expect(stateManager.onErrorCalled, isTrue); + expect(stateManager.lastError, isA()); + expect(stateManager.lastStackTrace, isNotNull); + }); + + test('should check shouldEmit', () { + // arrange + var shouldEmitCalled = false; + + // act + stateManager.shouldEmitOverride = (current, next) { + shouldEmitCalled = true; + return true; + }; + + // act + testUtil.emit(1); + + // assert + expect(shouldEmitCalled, isTrue); + }); + + test('should emit state if it is the same as the current state', () { + // arrange + const expectedState = 0; + + // act + testUtil.emit(expectedState); + + // assert + expect(stateManager.state, same(expectedState)); + expect(stateManager.onChangeCalled, isFalse); + }); + + test('should emit state if it is not the same as the current state', () { + // arrange + const expectedState = 1; + + // act + testUtil.emit(expectedState); + + // assert + expect(stateManager.state, same(expectedState)); + expect(stateManager.onChangeCalled, isTrue); + expect(stateManager.prevState, equals(initialState)); + expect(stateManager.curState, equals(expectedState)); + }); + }); + + group('method close', () { + late MockFunctionHandler handler; + + setUp(() { + handler = MockFunctionHandler(); + }); + + test('should call handler.close', () async { + // arrange + final stateManager = TestStateManager( + initialState, + handler: handler, + ); + + // act + await stateManager.close(); + + // assert + verify(() => handler.close()).called(1); + }); + + test('should call observer.onClose', () async { + // arrange + final stateManager = TestStateManager(initialState); + + // act + await stateManager.close(); + + // assert + expect(stateManager.onCloseCalled, isTrue); + }); + + test('should isClosed return true', () async { + // arrange + final stateManager = TestStateManager(initialState); + + // assert + expect(stateManager.isClosed, isFalse); + + // act + await stateManager.close(); + + // assert + expect(stateManager.isClosed, isTrue); + }); + }); + + group('method shouldEmit', () { + late TestStateManager stateManager; + late StateManagerBaseTestUtil testUtil; + + setUp(() { + stateManager = TestStateManager(initialState); + testUtil = StateManagerBaseTestUtil(stateManager); + }); + + tearDown(() => stateManager.close()); + + test('should emit state if shouldEmitOverride returns true', () { + // arrange + stateManager.shouldEmitOverride = (current, next) => true; + const expectedState = 1; + + // act + testUtil.emit(expectedState); + + // assert + expect(stateManager.onChangeCalled, isTrue); + expect(stateManager.prevState, equals(initialState)); + expect(stateManager.curState, equals(expectedState)); + }); + + test('should not emit state if shouldEmitOverride returns false', () { + // arrange + stateManager.shouldEmitOverride = (current, next) => false; + const expectedState = 1; + + // act + testUtil.emit(expectedState); + + // assert + expect(stateManager.onChangeCalled, isFalse); + expect(stateManager.state, equals(initialState)); + }); + + test('should call onError if shouldEmitOverride throws an error', () { + // arrange + final error = Exception('Test error'); + stateManager.shouldEmitOverride = (_, __) => throw error; + + // act & assert + expect(() => testUtil.emit(1), throwsException); + expect(stateManager.onErrorCalled, isTrue); + expect(stateManager.lastError, equals(error)); + expect(stateManager.lastStackTrace, isNotNull); + }); + }); + }); +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state_flutter/.gitignore b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state_flutter/.gitignore new file mode 100644 index 0000000..0fae679 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state_flutter/.gitignore @@ -0,0 +1,42 @@ +# Ignoring native folders of the example as they can be re-generated easily using: +# flutter create --platforms=android,ios,web,windows,macos . +example/android/ +example/ios/ +example/web/ +example/windows/ +example/macos/ +example/linux/ + +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +**/doc/api/ +.dart_tool/ +build/ +/pubspec.lock +.arcignore +.clineignore +.cursorignore +.rooignore diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state_flutter/AUTHORS b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state_flutter/AUTHORS new file mode 100644 index 0000000..0280e4e --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state_flutter/AUTHORS @@ -0,0 +1,13 @@ +The following authors have created the source code of "yx_state" published and distributed by YANDEX LLC as the owner: +kltsv +guid-empty +Vorkytaka +sorokinDev +wwwhttpru + +The following authors have licensed their contributions to YANDEX LLC and everyone who uses "yx_state" under the licensing terms detailed in LICENSE available at https://github.com/yandex/yx_state/blob/main/LICENSE. +kltsv +guid-empty +Vorkytaka +sorokinDev +wwwhttpru diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state_flutter/CHANGELOG.md b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state_flutter/CHANGELOG.md new file mode 100644 index 0000000..effe43c --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state_flutter/CHANGELOG.md @@ -0,0 +1,3 @@ +## 1.0.0 + +- Initial version. diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state_flutter/CONTRIBUTING.md b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state_flutter/CONTRIBUTING.md new file mode 100644 index 0000000..3cef4d8 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state_flutter/CONTRIBUTING.md @@ -0,0 +1,31 @@ +## Notice to external contributors +### General info +Hello! In order for us (YANDEX LLC) to accept patches and other contributions from you, you will have to adopt our Contributor License Agreement (the “CLA”). The current version of the CLA you may find here: + +* https://yandex.ru/legal/cla/?lang=en (in English) +* https://yandex.ru/legal/cla/?lang=ru (in Russian). + +By adopting the CLA, you state the following: + +* You obviously wish and are willingly licensing your contributions to us for our open source projects under the terms of the CLA, +* You have read the terms and conditions of the CLA and agree with them in full, +* You are legally able to provide and license your contributions as stated, +* We may use your contributions for our open source projects and for any other our project too, +* We rely on your assurances concerning the rights of third parties in relation to your contributions. + +If you agree with these principles, please read and adopt our CLA. By providing us your contributions, you hereby declare that you have read and adopted our CLA, and we may freely merge your contributions with our corresponding open source project and use it in further in accordance with terms and conditions of the CLA. + +### Provide contributions +If you have adopted terms and conditions of the CLA, you are able to provide your contributions. When you submit your pull request, please add the following information into it: + +``` +I hereby agree to the terms of the CLA available at: [link]. +``` + +Replace the bracketed text as follows: + +* [link] is the link at the current version of the CLA (you may add here a link https://yandex.ru/legal/cla/?lang=en (in English) or a link https://yandex.ru/legal/cla/?lang=ru (in Russian). + It is enough to provide us with such notification once. + +### Other questions +If you have any questions, please write us at opensource@yandex-team.ru. diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state_flutter/LICENSE b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state_flutter/LICENSE new file mode 100644 index 0000000..1734596 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state_flutter/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2025 YANDEX LLC + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state_flutter/README.md b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state_flutter/README.md new file mode 100644 index 0000000..eb3a5f7 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state_flutter/README.md @@ -0,0 +1,81 @@ +# yx_state_flutter + +Flutter widgets for [yx_state](https://github.com/yandex/yx_state). + +## Installation + +Add this package to your `pubspec.yaml` file: + +```yaml +dependencies: + yx_state_flutter: +``` + +## Widgets + +The package provides several widgets to help you manage state in your Flutter application: + +### StateBuilder + +A widget that rebuilds its UI in response to state changes. + +```dart +StateBuilder( + stateReadable: loginController, + builder: (context, state, child) { + return Text('Current status: ${state.status}'); + }, +) +``` + +### StateListener + +A widget that performs side effects in response to state changes without rebuilding the UI. + +```dart +StateListener( + stateReadable: loginController, + listener: (context, state) { + if (state.status == LoginStatus.error) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(state.errorMessage ?? 'Error')), + ); + } + }, + ... +) +``` + +### StateConsumer + +A widget that combines both StateBuilder and StateListener functionality. + +```dart +StateConsumer( + stateReadable: loginController, + listener: (context, state) { + if (state.status == LoginStatus.success) { + Navigator.of(context).pushReplacementNamed('/home'); + } + }, + builder: (context, state, child) { + return LoginForm(isLoading: state.status == LoginStatus.loading); + }, +) +``` + +### StateSelector + +A widget that rebuilds only when specific parts of the state change. + +```dart +StateSelector( + stateReadable: loginController, + selector: (state) => state.errorMessage, + builder: (context, errorMessage, child) { + return errorMessage != null + ? Text(errorMessage, style: TextStyle(color: Colors.red)) + : SizedBox.shrink(); + }, +) +``` diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state_flutter/analysis_options.yaml b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state_flutter/analysis_options.yaml new file mode 100644 index 0000000..f04c6cf --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state_flutter/analysis_options.yaml @@ -0,0 +1 @@ +include: ../../analysis_options.yaml diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state_flutter/example/.gitignore b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state_flutter/example/.gitignore new file mode 100644 index 0000000..0fae679 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state_flutter/example/.gitignore @@ -0,0 +1,42 @@ +# Ignoring native folders of the example as they can be re-generated easily using: +# flutter create --platforms=android,ios,web,windows,macos . +example/android/ +example/ios/ +example/web/ +example/windows/ +example/macos/ +example/linux/ + +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +**/doc/api/ +.dart_tool/ +build/ +/pubspec.lock +.arcignore +.clineignore +.cursorignore +.rooignore diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state_flutter/example/README.md b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state_flutter/example/README.md new file mode 100644 index 0000000..03e75df --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state_flutter/example/README.md @@ -0,0 +1 @@ +# An example for yx_state_flutter diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state_flutter/example/analysis_options.yaml b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state_flutter/example/analysis_options.yaml new file mode 100644 index 0000000..e2badd7 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state_flutter/example/analysis_options.yaml @@ -0,0 +1 @@ +include: ../../../analysis_options.yaml diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state_flutter/example/lib/main.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state_flutter/example/lib/main.dart new file mode 100644 index 0000000..c53d7de --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state_flutter/example/lib/main.dart @@ -0,0 +1,79 @@ +import 'package:flutter/material.dart'; +import 'package:yx_state/yx_state.dart'; +import 'package:yx_state_flutter/yx_state_flutter.dart'; + +void main() => runApp(const MyApp()); + +class CounterStateManager extends StateManager { + CounterStateManager(super.state); + + void increment() => handle((emit) async { + emit(state + 1); + }); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Flutter Demo', + debugShowCheckedModeBanner: false, + theme: ThemeData(colorSchemeSeed: Colors.blue), + home: const MyHomePage(title: 'Flutter Demo Home Page'), + ); + } +} + +class MyHomePage extends StatefulWidget { + final String title; + + const MyHomePage({super.key, required this.title}); + + @override + State createState() => _MyHomePageState(); +} + +class _MyHomePageState extends State { + late final CounterStateManager _stateManager; + + @override + void initState() { + super.initState(); + _stateManager = CounterStateManager(0); + } + + @override + void dispose() { + _stateManager.close(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: Text(widget.title)), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text('You have pushed the button this many times:'), + StateBuilder( + stateReadable: _stateManager, + builder: (context, state, _) => Text( + '$state', + style: Theme.of(context).textTheme.headlineMedium, + ), + ), + ], + ), + ), + floatingActionButton: FloatingActionButton( + onPressed: _stateManager.increment, + tooltip: 'Increment', + child: const Icon(Icons.add), + ), + ); + } +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state_flutter/example/pubspec.yaml b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state_flutter/example/pubspec.yaml new file mode 100644 index 0000000..77b94d4 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state_flutter/example/pubspec.yaml @@ -0,0 +1,22 @@ +name: yx_state_flutter_example +description: "An example for yx_state_flutter" +version: 1.0.0 +publish_to: 'none' + +environment: + sdk: ">=2.19.0 <4.0.0" + +dependencies: + flutter: + sdk: flutter + yx_state: + path: ../../yx_state + yx_state_flutter: + path: ../ + +dev_dependencies: + flutter_test: + sdk: flutter + +flutter: + uses-material-design: true diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state_flutter/example/pubspec_overrides.yaml b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state_flutter/example/pubspec_overrides.yaml new file mode 100644 index 0000000..73ec793 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state_flutter/example/pubspec_overrides.yaml @@ -0,0 +1,3 @@ +dependency_overrides: + yx_state: + path: ../../yx_state diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state_flutter/lib/src/state_builder.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state_flutter/lib/src/state_builder.dart new file mode 100644 index 0000000..61e36c4 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state_flutter/lib/src/state_builder.dart @@ -0,0 +1,135 @@ +import 'package:flutter/widgets.dart'; +import 'package:yx_state/yx_state.dart'; + +import 'state_consumer.dart'; +import 'state_listener.dart'; +import 'state_selector.dart'; +import 'typedefs.dart'; + +/// A widget that builds itself based on the latest snapshot of interaction with a [StateReadable]. +/// +/// [StateBuilder] automatically rebuilds whenever the state from its [stateReadable] changes. +/// It's used for UI components that need to reflect the current state in their presentation. +/// +/// The [builder] function is called whenever the state changes and the optional [buildWhen] +/// condition returns true. +/// +/// {@tool snippet} +/// This example shows how to use [StateBuilder] to display a counter: +/// +/// ```dart +/// class CounterWidget extends StatelessWidget { +/// final CounterReadable counterReadable; +/// +/// const CounterWidget({ +/// required this.counterReadable, +/// super.key, +/// }); +/// +/// @override +/// Widget build(BuildContext context) => StateBuilder( +/// stateReadable: counterReadable, +/// buildWhen: (previous, current) => previous != current, // Rebuild only when the value changes +/// builder: (context, count, _) => Text( +/// 'Count: $count', +/// style: TextStyle(fontSize: 24), +/// ), +/// ); +/// } +/// +/// class CounterReadable implements StateReadable { +/// final StreamController _controller = StreamController.broadcast(); +/// int _count = 0; +/// +/// @override +/// int get state => _count; +/// +/// @override +/// Stream get stream => _controller.stream; +/// +/// void increment() { +/// _count++; +/// _controller.add(_count); +/// } +/// +/// Future dispose() => _controller.close(); +/// } +/// ``` +/// {@end-tool} +/// +/// See also: +/// +/// * [StateListener], which performs side effects in response to state changes without rebuilding. +/// * [StateConsumer], which combines both rebuilding and side effects. +/// * [StateSelector], which rebuilds only when a specific part of the state changes. +class StateBuilder extends StatefulWidget { + /// Creates a new [StateBuilder]. + /// + /// The [stateReadable] provides the state that this widget will respond to. + /// The [builder] creates a widget subtree based on the current state. + /// The optional [buildWhen] determines when the [builder] should rebuild. + const StateBuilder({ + required this.stateReadable, + required this.builder, + this.buildWhen, + this.child, + super.key, + }); + + /// The source of the state. + /// + /// This state readable object provides both the current state and a stream of state updates. + /// The [builder] will be called whenever this source emits a new state. + final StateReadable stateReadable; + + /// The builder that builds a widget based on the current state. + /// + /// This function is called with the [BuildContext], the current state, and + /// an optional [child] widget whenever the state changes and [buildWhen] returns true. + /// The [child] parameter can be used for optimization by passing widgets that + /// don't depend on the state. + final StateWidgetBuilder builder; + + /// Optional condition to determine when the [builder] should rebuild. + /// + /// If null, the builder will rebuild on every state change. + /// If provided, it will be called with the previous and current state, + /// and only if it returns true will [builder] be called. + final StateBuilderCondition? buildWhen; + + /// The child of the [StateBuilder]. + /// + /// If provided, the [builder] will be called with this child as an argument. + /// This is useful for optimizing rebuilds when part of the widget subtree + /// doesn't depend on the state. + final Widget? child; + + @override + State> createState() => _StateBuilderState(); +} + +class _StateBuilderState extends State> { + late S _state; + + @override + void initState() { + super.initState(); + _state = widget.stateReadable.state; + } + + @override + void didUpdateWidget(StateBuilder oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.stateReadable != widget.stateReadable) { + _state = widget.stateReadable.state; + } + } + + @override + Widget build(BuildContext context) => StateListener( + stateReadable: widget.stateReadable, + listenWhen: widget.buildWhen, + listener: (context, state) => setState(() => _state = state), + child: widget.builder(context, _state, widget.child), + ); +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state_flutter/lib/src/state_consumer.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state_flutter/lib/src/state_consumer.dart new file mode 100644 index 0000000..f454f2a --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state_flutter/lib/src/state_consumer.dart @@ -0,0 +1,144 @@ +import 'package:flutter/widgets.dart'; +import 'package:yx_state/yx_state.dart'; + +import 'state_builder.dart'; +import 'state_listener.dart'; +import 'state_selector.dart'; +import 'typedefs.dart'; + +/// A widget that combines [StateBuilder] and [StateListener]. +/// +/// [StateConsumer] is useful when you need a widget that both rebuilds UI and +/// performs side effects (like navigation, showing dialogs) in response to state changes. +/// +/// {@tool snippet} +/// This example shows how to use [StateConsumer] for a login form: +/// +/// ```dart +/// // Using StateConsumer in a widget +/// class LoginScreen extends StatelessWidget { +/// final LoginController loginController; +/// +/// const LoginScreen({ +/// required this.loginController, +/// super.key, +/// }); +/// +/// @override +/// Widget build(BuildContext context) => Scaffold( +/// appBar: AppBar(title: Text('Login')), +/// body: StateConsumer( +/// stateReadable: loginController, +/// listener: (context, state) { +/// if (state.status == LoginStatus.success) { +/// Navigator.of(context).pushReplacementNamed('/home'); +/// } else if (state.status == LoginStatus.error) { +/// ScaffoldMessenger.of(context).showSnackBar( +/// SnackBar(content: Text(state.errorMessage ?? 'Login failed')), +/// ); +/// } +/// }, +/// builder: (context, state, _) => LoginForm( +/// isLoading: state.status == LoginStatus.loading, +/// ), +/// ), +/// ); +/// } +/// +/// // A simple login form widget +/// class LoginForm extends StatelessWidget { +/// final bool isLoading; +/// +/// const LoginForm({ +/// required this.isLoading, +/// super.key, +/// }); +/// +/// @override +/// Widget build(BuildContext context) => Center( +/// child: isLoading +/// ? const CircularProgressIndicator() +/// : const Text('Login Form'), +/// ); +/// } +/// +/// // Example state and state readable +/// enum LoginStatus { initial, loading, success, error } +/// +/// class LoginState { +/// final LoginStatus status; +/// final String? errorMessage; +/// +/// const LoginState({ +/// this.status = LoginStatus.initial, +/// this.errorMessage, +/// }); +/// } +/// +/// // A state readable implementation for login +/// class LoginController implements StateReadable { +/// // Implementation details... +/// +/// @override +/// LoginState get state => const LoginState(); // Example implementation +/// +/// @override +/// Stream get stream => Stream.empty(); // Example implementation +/// } +/// ``` +/// {@end-tool} +/// +/// See also: +/// +/// * [StateBuilder], which only rebuilds the UI in response to state changes. +/// * [StateListener], which only performs side effects in response to state changes. +/// * [StateSelector], which rebuilds only when a specific part of the state changes. +class StateConsumer extends StatelessWidget { + /// Creates a new [StateConsumer]. + const StateConsumer({ + required this.stateReadable, + required this.builder, + required this.listener, + this.buildWhen, + this.listenWhen, + this.child, + super.key, + }); + + /// The source of the state. + final StateReadable stateReadable; + + /// The builder that builds a widget based on the current state. + final StateWidgetBuilder builder; + + /// The function that is called when the state changes. + final StateWidgetListener listener; + + /// Optional condition to determine when the [builder] should rebuild. + /// If null, the builder will rebuild on every state change. + final StateBuilderCondition? buildWhen; + + /// Optional condition to determine when to call [listener]. + /// If null, the listener will be called on every state change. + final StateListenerCondition? listenWhen; + + /// The child of the [StateConsumer]. + /// + /// If provided, the [builder] will be called with this child as an argument. + /// This is useful for optimizing rebuilds when part of the widget subtree + /// doesn't depend on the state. + final Widget? child; + + @override + Widget build(BuildContext context) => StateBuilder( + stateReadable: stateReadable, + builder: builder, + buildWhen: (previous, current) { + if (listenWhen?.call(previous, current) ?? true) { + listener(context, current); + } + return buildWhen?.call(previous, current) ?? true; + }, + child: child, + ); +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state_flutter/lib/src/state_listener.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state_flutter/lib/src/state_listener.dart new file mode 100644 index 0000000..71b9e9f --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state_flutter/lib/src/state_listener.dart @@ -0,0 +1,172 @@ +import 'dart:async'; + +import 'package:flutter/widgets.dart'; +import 'package:yx_state/yx_state.dart'; + +import 'state_builder.dart'; +import 'state_consumer.dart'; +import 'state_selector.dart'; +import 'typedefs.dart'; + +/// A widget that listens to a [StateReadable] and calls a listener when the state changes. +/// +/// [StateListener] is used for performing side effects in response to state changes +/// such as showing dialogs, navigating to another screen, or logging events. It does not rebuild +/// its child when the state changes. +/// +/// The [listener] function is called whenever the state changes and the optional [listenWhen] +/// condition returns true. +/// +/// {@tool snippet} +/// This example shows how to use [StateListener] to log state changes and perform navigation: +/// +/// ```dart +/// class CounterWidget extends StatelessWidget { +/// final CounterReadable counterReadable; +/// final NavigatorState navigator; +/// +/// const CounterWidget({ +/// required this.counterReadable, +/// required this.navigator, +/// super.key, +/// }); +/// +/// @override +/// Widget build(BuildContext context) => StateListener( +/// stateReadable: counterReadable, +/// listenWhen: (previous, current) => current > 5, // Only listen when counter exceeds 5 +/// listener: (context, count) { +/// // Log the current count +/// debugPrint('Counter reached milestone: $count'); +/// +/// // Navigate to a different screen when count reaches 10 +/// if (count >= 10) { +/// navigator.pushNamed('/milestone-reached'); +/// } +/// }, +/// child: const SizedBox(), // A simple placeholder widget +/// ); +/// } +/// +/// +/// class CounterReadable implements StateReadable { +/// final StreamController _controller = StreamController.broadcast(); +/// int _count = 0; +/// +/// @override +/// int get state => _count; +/// +/// @override +/// Stream get stream => _controller.stream; +/// +/// void increment() { +/// _count++; +/// _controller.add(_count); +/// } +/// +/// Future dispose() => _controller.close(); +/// } +/// ``` +/// {@end-tool} +/// +/// See also: +/// +/// * [StateBuilder], which rebuilds when the state changes instead of performing side effects. +/// * [StateConsumer], which combines both rebuilding and side effects. +/// * [StateSelector], which rebuilds only when a specific part of the state changes. +class StateListener extends StatefulWidget { + /// Creates a new [StateListener]. + /// + /// The [stateReadable], [listener], and [child] parameters must not be null. + const StateListener({ + required this.stateReadable, + required this.listener, + required this.child, + this.listenWhen, + super.key, + }); + + /// The source of the state. + /// + /// This state readable object provides both the current state and a stream of state updates. + /// The [listener] will be called whenever this source emits a new state. + final StateReadable stateReadable; + + /// The function that is called when the state changes. + /// + /// This function is called with the [BuildContext] and the current state + /// whenever the state changes and [listenWhen] returns true. + /// Use this for performing side effects like showing dialogs or navigating. + final StateWidgetListener listener; + + /// The widget below this widget in the tree. + /// + /// This widget and its descendants will not rebuild when the state changes. + /// For a widget that rebuilds with state changes, use [StateBuilder]. + final Widget child; + + /// Optional condition to determine when to call [listener]. + /// + /// If null, the listener will be called on every state change. + /// If provided, it will be called with the previous and current state, + /// and only if it returns true will [listener] be called. + final StateListenerCondition? listenWhen; + + @override + State> createState() => _StateListenerState(); +} + +/// The state for the [StateListener] widget. +class _StateListenerState extends State> { + StreamSubscription? _subscription; + late S _previousState; + late StateReadable _currentStateReadable; + + @override + void initState() { + super.initState(); + _currentStateReadable = widget.stateReadable; + _previousState = _currentStateReadable.state; + _subscribe(); + } + + @override + void didUpdateWidget(StateListener oldWidget) { + super.didUpdateWidget(oldWidget); + final stateReadable = widget.stateReadable; + if (oldWidget.stateReadable != stateReadable) { + _unsubscribe(); + _currentStateReadable = stateReadable; + _previousState = _currentStateReadable.state; + _subscribe(); + } + } + + void _subscribe() { + _subscription = _currentStateReadable.stream.listen((state) { + if (!mounted) { + return; + } + + if (widget.listenWhen?.call(_previousState, state) ?? true) { + widget.listener(context, state); + } + + _previousState = state; + }); + } + + void _unsubscribe() { + _subscription?.cancel(); + _subscription = null; + } + + @override + void dispose() { + _unsubscribe(); + super.dispose(); + } + + @override + Widget build(BuildContext context) => widget.child; +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state_flutter/lib/src/state_selector.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state_flutter/lib/src/state_selector.dart new file mode 100644 index 0000000..1242ddf --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state_flutter/lib/src/state_selector.dart @@ -0,0 +1,137 @@ +import 'package:flutter/widgets.dart'; +import 'package:yx_state/yx_state.dart'; + +import 'state_builder.dart'; +import 'state_consumer.dart'; +import 'state_listener.dart'; +import 'typedefs.dart'; + +/// A widget that builds itself based on a part of the state from a [StateReadable]. +/// +/// [StateSelector] allows you to optimize rebuilds by only rebuilding when a specific +/// part of the state changes, selected by the [selector] function. +/// +/// {@tool snippet} +/// This example shows how to use [StateSelector] to only rebuild when a specific +/// property of the state changes: +/// +/// ```dart +/// class UserForm extends StatelessWidget { +/// final UserController userController; +/// +/// const UserForm({ +/// required this.userController, +/// super.key, +/// }); +/// +/// @override +/// Widget build(BuildContext context) => StateSelector( +/// stateReadable: userController, +/// selector: (state) => state.isLoading, +/// builder: (context, isLoading, _) => isLoading +/// ? const CircularProgressIndicator() +/// : const Text('User loaded'), +/// ); +/// } +/// +/// // A state readable implementation for user +/// class UserController implements StateReadable { +/// // Implementation details... +/// +/// @override +/// UserState get state => const UserState(); // Example implementation +/// +/// @override +/// Stream get stream => Stream.empty(); // Example implementation +/// } +/// +/// // Assume we have a complex state with multiple properties +/// class UserState { +/// final String name; +/// final bool isLoading; +/// final List permissions; +/// +/// const UserState({ +/// this.name = '', +/// this.isLoading = false, +/// this.permissions = const [], +/// }); +/// } +/// ``` +/// {@end-tool} +/// +/// See also: +/// +/// * [StateBuilder], which rebuilds the UI when the entire state changes. +/// * [StateListener], which performs side effects in response to state changes. +/// * [StateConsumer], which combines both rebuilding and side effects. +class StateSelector extends StatefulWidget { + /// Creates a new [StateSelector]. + const StateSelector({ + required this.stateReadable, + required this.selector, + required this.builder, + this.child, + super.key, + }); + + /// The source of the state. + final StateReadable stateReadable; + + /// The function that extracts a value from the state. + /// + /// This function is used to select a specific part of the state to watch for changes. + /// The widget will only rebuild when the selected value changes. + final StateWidgetSelector selector; + + /// The builder that builds a widget based on the selected value. + /// + /// This function is called with the selected value whenever it changes. + final StateWidgetBuilder builder; + + /// The child of the widget. + /// + /// If provided, the [builder] will be called with this child as an argument. + /// This is useful for optimizing rebuilds when part of the widget subtree + /// doesn't depend on the selected value. + final Widget? child; + + @override + State> createState() => _StateSelectorState(); +} + +class _StateSelectorState extends State> { + late StateReadable _currentStateReadable; + late U _selectedValue; + + @override + void initState() { + super.initState(); + _currentStateReadable = widget.stateReadable; + _selectedValue = widget.selector(_currentStateReadable.state); + } + + @override + void didUpdateWidget(StateSelector oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.stateReadable != widget.stateReadable) { + _currentStateReadable = widget.stateReadable; + _selectedValue = widget.selector(_currentStateReadable.state); + } else if (oldWidget.selector != widget.selector) { + _selectedValue = widget.selector(_currentStateReadable.state); + } + } + + @override + Widget build(BuildContext context) => StateListener( + stateReadable: _currentStateReadable, + listener: (context, state) { + final newValue = widget.selector(state); + + if (newValue != _selectedValue) { + setState(() => _selectedValue = newValue); + } + }, + child: widget.builder(context, _selectedValue, widget.child), + ); +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state_flutter/lib/src/typedefs.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state_flutter/lib/src/typedefs.dart new file mode 100644 index 0000000..db3de82 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state_flutter/lib/src/typedefs.dart @@ -0,0 +1,20 @@ +import 'package:flutter/widgets.dart'; + +/// Builder function that builds a widget based on the current state. +typedef StateWidgetBuilder = Widget Function( + BuildContext context, + S state, + Widget? child, +); + +/// Listener function that is called when the state changes. +typedef StateWidgetListener = void Function(BuildContext context, S state); + +/// Function that determines when a widget should rebuild. +typedef StateBuilderCondition = bool Function(S previous, S current); + +/// Function that determines when a listener should be called. +typedef StateListenerCondition = bool Function(S previous, S current); + +/// Function that extracts a value from the state. +typedef StateWidgetSelector = T Function(S state); diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state_flutter/lib/yx_state_flutter.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state_flutter/lib/yx_state_flutter.dart new file mode 100644 index 0000000..08ed988 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state_flutter/lib/yx_state_flutter.dart @@ -0,0 +1,8 @@ +/// Flutter widgets for [yx_state](https://pub.dev/packages/yx_state). +library yx_state_flutter; + +export 'src/state_builder.dart'; +export 'src/state_consumer.dart'; +export 'src/state_listener.dart'; +export 'src/state_selector.dart'; +export 'src/typedefs.dart'; diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state_flutter/pubspec.yaml b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state_flutter/pubspec.yaml new file mode 100644 index 0000000..fa86f62 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state_flutter/pubspec.yaml @@ -0,0 +1,21 @@ +name: yx_state_flutter +description: Flutter widgets for yx_state. +version: 1.0.0 +repository: https://github.com/yandex/city-services-pub/tree/main/yx_state/packages/yx_state_flutter +issue_tracker: https://github.com/yandex/city-services-pub/issues +topics: + - yx-state + - state-management + +environment: + sdk: '>=2.19.0 <4.0.0' + +dependencies: + yx_state: ^1.0.0 + flutter: + sdk: flutter + +dev_dependencies: + flutter_test: + sdk: flutter + diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state_flutter/pubspec_overrides.yaml b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state_flutter/pubspec_overrides.yaml new file mode 100644 index 0000000..139ecd1 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state_flutter/pubspec_overrides.yaml @@ -0,0 +1,3 @@ +dependency_overrides: + yx_state: + path: ../yx_state diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state_transformers/.gitignore b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state_transformers/.gitignore new file mode 100644 index 0000000..8267c1b --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state_transformers/.gitignore @@ -0,0 +1,33 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +**/doc/api/ +.dart_tool/ +build/ +/pubspec.lock +.arcignore +.clineignore +.cursorignore +.rooignore diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state_transformers/AUTHORS b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state_transformers/AUTHORS new file mode 100644 index 0000000..0280e4e --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state_transformers/AUTHORS @@ -0,0 +1,13 @@ +The following authors have created the source code of "yx_state" published and distributed by YANDEX LLC as the owner: +kltsv +guid-empty +Vorkytaka +sorokinDev +wwwhttpru + +The following authors have licensed their contributions to YANDEX LLC and everyone who uses "yx_state" under the licensing terms detailed in LICENSE available at https://github.com/yandex/yx_state/blob/main/LICENSE. +kltsv +guid-empty +Vorkytaka +sorokinDev +wwwhttpru diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state_transformers/CHANGELOG.md b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state_transformers/CHANGELOG.md new file mode 100644 index 0000000..effe43c --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state_transformers/CHANGELOG.md @@ -0,0 +1,3 @@ +## 1.0.0 + +- Initial version. diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state_transformers/CONTRIBUTING.md b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state_transformers/CONTRIBUTING.md new file mode 100644 index 0000000..3cef4d8 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state_transformers/CONTRIBUTING.md @@ -0,0 +1,31 @@ +## Notice to external contributors +### General info +Hello! In order for us (YANDEX LLC) to accept patches and other contributions from you, you will have to adopt our Contributor License Agreement (the “CLA”). The current version of the CLA you may find here: + +* https://yandex.ru/legal/cla/?lang=en (in English) +* https://yandex.ru/legal/cla/?lang=ru (in Russian). + +By adopting the CLA, you state the following: + +* You obviously wish and are willingly licensing your contributions to us for our open source projects under the terms of the CLA, +* You have read the terms and conditions of the CLA and agree with them in full, +* You are legally able to provide and license your contributions as stated, +* We may use your contributions for our open source projects and for any other our project too, +* We rely on your assurances concerning the rights of third parties in relation to your contributions. + +If you agree with these principles, please read and adopt our CLA. By providing us your contributions, you hereby declare that you have read and adopted our CLA, and we may freely merge your contributions with our corresponding open source project and use it in further in accordance with terms and conditions of the CLA. + +### Provide contributions +If you have adopted terms and conditions of the CLA, you are able to provide your contributions. When you submit your pull request, please add the following information into it: + +``` +I hereby agree to the terms of the CLA available at: [link]. +``` + +Replace the bracketed text as follows: + +* [link] is the link at the current version of the CLA (you may add here a link https://yandex.ru/legal/cla/?lang=en (in English) or a link https://yandex.ru/legal/cla/?lang=ru (in Russian). + It is enough to provide us with such notification once. + +### Other questions +If you have any questions, please write us at opensource@yandex-team.ru. diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state_transformers/LICENSE b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state_transformers/LICENSE new file mode 100644 index 0000000..1734596 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state_transformers/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2025 YANDEX LLC + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state_transformers/README.md b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state_transformers/README.md new file mode 100644 index 0000000..33cb6ab --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state_transformers/README.md @@ -0,0 +1,55 @@ +# yx_state_transformers + +Custom task transformers for [yx_state](https://pub.dev/packages/yx_state). + +## Concurrency Strategies + +yx_state_transformers provides several concurrency strategies for handling tasks: + +1. **Sequential** - Process tasks one after another in order: + +```dart +class CounterManager extends StateManager { + CounterManager() + : super( + const CounterState(0), + handler: sequential(), + ); +} +``` + +2. **Concurrent** - Process tasks in parallel: + +```dart +class CounterManager extends StateManager { + CounterManager() + : super( + const CounterState(0), + handler: concurrent(), + ); +} +``` + +3. **Droppable** - Ignore new tasks while processing: + +```dart +class CounterManager extends StateManager { + CounterManager() + : super( + const CounterState(0), + handler: droppable(), + ); +} +``` + +4. **Restartable** - Cancel current task when a new one comes in: + +```dart +class CounterManager extends StateManager { + CounterManager() + : super( + const CounterState(0), + handler: restartable(), + ); +} +``` diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state_transformers/analysis_options.yaml b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state_transformers/analysis_options.yaml new file mode 100644 index 0000000..f04c6cf --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state_transformers/analysis_options.yaml @@ -0,0 +1 @@ +include: ../../analysis_options.yaml diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state_transformers/example/main.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state_transformers/example/main.dart new file mode 100644 index 0000000..664ba59 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state_transformers/example/main.dart @@ -0,0 +1,41 @@ +import 'package:yx_state/yx_state.dart'; +import 'package:yx_state_transformers/yx_state_transformers.dart'; + +void main() async { + /// Create a state manager with a concurrent handler. + final counter = CounterStateManager(0); + + /// Subscribe to state changes and print each state. + final subscription = counter.stream.listen(print); + + counter.increment(); + counter.incrementBy(8); + counter.increment(); + + // wait for 3 seconds + await Future.delayed(const Duration(seconds: 3)); + + /// Close the state manager. + await counter.close(); + + /// Unsubscribe from state changes. + await subscription.cancel(); +} + +class CounterStateManager extends StateManager { + CounterStateManager(super.state) : super(handler: concurrent()); + + void increment() => handle( + (emit) async { + await Future.delayed(const Duration(seconds: 2)); + emit(state + 1); + }, + ); + + void incrementBy(int value) => handle( + (emit) async { + await Future.delayed(const Duration(seconds: 1)); + emit(state + value); + }, + ); +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state_transformers/lib/src/function_handlers.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state_transformers/lib/src/function_handlers.dart new file mode 100644 index 0000000..7a6bb71 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state_transformers/lib/src/function_handlers.dart @@ -0,0 +1,34 @@ +import 'package:yx_state/yx_state.dart'; + +import 'handle_task_transformers.dart'; + +/// Process tasks one at a time by maintaining a queue of added tasks +/// and processing the tasks sequentially. +/// +/// This is the default and safest handler to use in most cases. +FunctionHandler sequential() => + StreamFunctionHandler( + handleTransformer: HandleTaskTransformers.sequential()); + +/// Process tasks concurrently without any restrictions. +/// +/// Use with caution as concurrent state updates may lead to race conditions. +FunctionHandler concurrent() => + StreamFunctionHandler( + handleTransformer: HandleTaskTransformers.concurrent()); + +/// Process only one task and ignore (drop) any new tasks +/// until the current task is done. +/// +/// Useful for preventing spamming of state updates during ongoing operations. +FunctionHandler droppable() => + StreamFunctionHandler( + handleTransformer: HandleTaskTransformers.droppable()); + +/// Process only one task by cancelling any pending tasks and +/// processing the new task immediately. +/// +/// Useful for operations where only the latest request matters. +FunctionHandler restartable() => + StreamFunctionHandler( + handleTransformer: HandleTaskTransformers.restartable()); diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state_transformers/lib/src/handle_task_transformers.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state_transformers/lib/src/handle_task_transformers.dart new file mode 100644 index 0000000..d2ace87 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state_transformers/lib/src/handle_task_transformers.dart @@ -0,0 +1,92 @@ +import 'dart:async'; + +import 'package:stream_transform/stream_transform.dart'; +import 'package:yx_state/yx_state.dart'; + +/// {@template task_transformers} +/// A collection of transformers for controlling how tasks are processed. +/// +/// These transformers implement different concurrency strategies to control how +/// state update tasks are processed. Each strategy provides different behavior +/// for handling multiple state updates that occur close together. +/// +/// Available strategies: +/// +/// - [sequential]: Process tasks one at a time in order (FIFO queue) +/// * Tasks are processed in the order they are received +/// * New tasks are queued until current task completes +/// * Best for when order of operations matters +/// +/// - [concurrent]: Process all tasks concurrently without restrictions +/// * All tasks run at the same time without waiting +/// * Can lead to race conditions if not used carefully +/// * Best for independent operations that don't affect each other +/// +/// - [droppable]: Process one task at a time, ignoring new tasks while busy +/// * If a task is running, new tasks are ignored (dropped) +/// * Prevents task queue buildup during rapid events +/// * Best for preventing spamming of expensive operations +/// +/// - [restartable]: Process one task at a time, cancelling current task when new arrives +/// * If a task is running, it gets cancelled when a new task arrives +/// * Only the latest task is processed to completion +/// * Best for search-as-you-type and other scenarios where only latest input matters +/// {@endtemplate} +abstract class HandleTaskTransformers { + static HandleTaskTransformer sequential() { + return (tasks, mapper) => tasks.asyncExpand>(mapper); + } + + static HandleTaskTransformer concurrent() { + return (tasks, mapper) => + tasks.concurrentAsyncExpand>(mapper); + } + + static HandleTaskTransformer droppable() { + return (tasks, mapper) => tasks + .transform>(_ExhaustMapStreamTransformer(mapper)); + } + + static HandleTaskTransformer restartable() { + return (tasks, mapper) => tasks.switchMap>(mapper); + } +} + +class _ExhaustMapStreamTransformer + extends StreamTransformerBase, HandleTask> { + _ExhaustMapStreamTransformer(this.mapper); + + final HandleTaskMapper mapper; + + @override + Stream> bind(Stream> stream) { + late StreamSubscription> subscription; + StreamSubscription>? mappedSubscription; + + final controller = StreamController>( + onCancel: () async { + await mappedSubscription?.cancel(); + return subscription.cancel(); + }, + sync: true, + ); + + subscription = stream.listen( + (task) { + if (mappedSubscription != null) { + task.cancel(); + return; + } + mappedSubscription = mapper(task).listen( + controller.add, + onError: controller.addError, + onDone: () => mappedSubscription = null, + ); + }, + onError: controller.addError, + onDone: () => mappedSubscription ?? controller.close(), + ); + + return controller.stream; + } +} diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state_transformers/lib/yx_state_transformers.dart b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state_transformers/lib/yx_state_transformers.dart new file mode 100644 index 0000000..2c7d5c6 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state_transformers/lib/yx_state_transformers.dart @@ -0,0 +1,7 @@ +/// Custom task transformers for [yx_state](https://pub.dev/packages/yx_state). +/// +/// This package provides various concurrency strategies for handling tasks +/// in state management operations. +library yx_state_transformers; + +export 'src/function_handlers.dart'; diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state_transformers/pubspec.yaml b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state_transformers/pubspec.yaml new file mode 100644 index 0000000..ab43560 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state_transformers/pubspec.yaml @@ -0,0 +1,18 @@ +name: yx_state_transformers +description: Custom task transformers for yx_state +version: 1.0.0 +repository: https://github.com/yandex/city-services-pub/tree/main/yx_state/packages/yx_state_transformers +issue_tracker: https://github.com/yandex/city-services-pub/issues +topics: + - yx-state + - state-management + - concurrency + +environment: + sdk: '>=2.19.0 <4.0.0' + +dependencies: + yx_state: ^1.0.0 + stream_transform: ^2.1.0 + +dev_dependencies: diff --git a/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state_transformers/pubspec_overrides.yaml b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state_transformers/pubspec_overrides.yaml new file mode 100644 index 0000000..139ecd1 --- /dev/null +++ b/mnemo_cards_web_v2/packages/yx/city-services-pub/yx_state/packages/yx_state_transformers/pubspec_overrides.yaml @@ -0,0 +1,3 @@ +dependency_overrides: + yx_state: + path: ../yx_state diff --git a/tools/deploy/install-forgejo-runner.sh b/tools/deploy/install-forgejo-runner.sh new file mode 100644 index 0000000..7829c9c --- /dev/null +++ b/tools/deploy/install-forgejo-runner.sh @@ -0,0 +1,110 @@ +#!/bin/bash + +# Script to install and setup Forgejo Actions Runner +# This is required for Actions to run + +set -e + +SERVER_IP="147.45.152.129" +SERVER_USER="root" + +echo "🚀 Installing Forgejo Actions Runner..." +echo "" +echo "⚠️ You will need a registration token from Forgejo Admin panel!" +echo " Get it from: https://code.mnemo-cards.online/admin/actions/runners" +echo "" +read -p "Do you have the registration token ready? (y/n) " -n 1 -r +echo +if [[ ! $REPLY =~ ^[Yy]$ ]]; then + echo "Please get the token first, then run this script again." + exit 1 +fi + +read -p "Enter the registration token: " TOKEN + +if [ -z "$TOKEN" ]; then + echo "❌ Token cannot be empty!" + exit 1 +fi + +echo "" +echo "📦 Installing runner on server..." + +ssh "$SERVER_USER@$SERVER_IP" << EOF + set -e + + echo "📥 Downloading act_runner..." + cd /tmp + wget -q --show-progress https://dl.gitea.com/act_runner/0.2.6/act_runner-0.2.6-linux-amd64 -O act_runner + chmod +x act_runner + mv act_runner /usr/local/bin/ + + echo "✅ act_runner installed" + act_runner --version + + echo "" + echo "📝 Creating runner directory..." + mkdir -p /var/lib/forgejo-runner + cd /var/lib/forgejo-runner + + echo "" + echo "🔐 Registering runner with token..." + /usr/local/bin/act_runner register --no-interactive \\ + --instance https://code.mnemo-cards.online \\ + --token $TOKEN \\ + --name default-runner \\ + --labels ubuntu-latest:docker://node:16-bullseye,ubuntu-22.04:docker://node:16-bullseye + + echo "" + echo "⚙️ Creating systemd service..." + cat > /etc/systemd/system/forgejo-runner.service << 'SERVICE' +[Unit] +Description=Forgejo Actions Runner +After=network.target +Wants=network.target + +[Service] +Type=simple +User=root +WorkingDirectory=/var/lib/forgejo-runner +ExecStart=/usr/local/bin/act_runner daemon --config /var/lib/forgejo-runner/.runner +Restart=always +RestartSec=5 +StandardOutput=journal +StandardError=journal + +[Install] +WantedBy=multi-user.target +SERVICE + + echo "" + echo "🔄 Enabling and starting service..." + systemctl daemon-reload + systemctl enable forgejo-runner + systemctl start forgejo-runner + + echo "" + echo "✅ Runner setup complete!" + echo "" + echo "📊 Status:" + systemctl status forgejo-runner --no-pager -l || true + + echo "" + echo "📝 Runner logs (last 20 lines):" + journalctl -u forgejo-runner -n 20 --no-pager || true + +EOF + +echo "" +echo "✅ Done! Forgejo Actions Runner is now running!" +echo "" +echo "🔍 Check runner status:" +echo " ssh root@$SERVER_IP 'systemctl status forgejo-runner'" +echo "" +echo "📝 View logs:" +echo " ssh root@$SERVER_IP 'journalctl -u forgejo-runner -f'" +echo "" +echo "🌐 Verify in Forgejo:" +echo " https://code.mnemo-cards.online/admin/actions/runners" +echo " You should see 'default-runner' online" + diff --git a/tools/deploy/setup-forgejo-actions.sh b/tools/deploy/setup-forgejo-actions.sh new file mode 100755 index 0000000..95d0ac0 --- /dev/null +++ b/tools/deploy/setup-forgejo-actions.sh @@ -0,0 +1,151 @@ +#!/bin/bash + +# Script to setup and diagnose Forgejo Actions +# Fixes common issues with actions not running + +set -e + +SERVER_IP="147.45.152.129" +SERVER_USER="root" +FORGEJO_DOMAIN="code.mnemo-cards.online" + +echo "🔧 Setting up Forgejo Actions..." + +ssh "$SERVER_USER@$SERVER_IP" << 'EOF' + set -e + + echo "📝 Checking Forgejo installation..." + + # Check if Forgejo is running + if systemctl is-active --quiet forgejo; then + echo "✅ Forgejo service is running" + else + echo "❌ Forgejo service is not running" + echo "Starting Forgejo..." + systemctl start forgejo + sleep 3 + fi + + # Check Forgejo configuration + FORGEJO_CONFIG="/etc/forgejo/app.ini" + if [ ! -f "$FORGEJO_CONFIG" ]; then + FORGEJO_CONFIG="/var/lib/forgejo/custom/conf/app.ini" + fi + + if [ ! -f "$FORGEJO_CONFIG" ]; then + echo "❌ Forgejo config not found. Checking possible locations..." + find / -name "app.ini" 2>/dev/null | grep -i forgejo || echo "Config not found" + exit 1 + fi + + echo "✅ Found Forgejo config: $FORGEJO_CONFIG" + + echo "" + echo "📊 Checking Actions configuration..." + + # Check if Actions are enabled + if grep -q "^\[actions\]" "$FORGEJO_CONFIG"; then + echo "✅ Actions section found in config" + grep -A 5 "^\[actions\]" "$FORGEJO_CONFIG" || true + else + echo "⚠️ Actions section not found in config" + echo "Adding Actions configuration..." + + cat >> "$FORGEJO_CONFIG" << 'CONFIG' + +[actions] +ENABLED = true +DEFAULT_ACTIONS_URL = https://code.forgejo.org +CONFIG + + echo "✅ Actions configuration added" + systemctl restart forgejo + sleep 5 + fi + + echo "" + echo "🏃 Checking for Forgejo Runner..." + + # Check if act runner is installed + if command -v act_runner &> /dev/null; then + echo "✅ act_runner is installed" + act_runner --version || true + else + echo "❌ act_runner is not installed" + echo "Installing act_runner..." + + # Download and install act_runner + cd /tmp + wget -q https://dl.gitea.com/act_runner/latest/act_runner-linux-amd64 -O act_runner + chmod +x act_runner + mv act_runner /usr/local/bin/ + + echo "✅ act_runner installed" + fi + + # Check if runner is registered + if systemctl is-active --quiet act_runner 2>/dev/null; then + echo "✅ Runner service is running" + else + echo "⚠️ Runner service is not running or not configured" + echo "" + echo "To register the runner:" + echo "1. Get registration token from Forgejo:" + echo " Site Admin → Actions → Runners → Create runner" + echo "" + echo "2. Register the runner:" + echo " act_runner register --no-interactive \\" + echo " --instance https://code.mnemo-cards.online \\" + echo " --token YOUR_TOKEN_HERE \\" + echo " --name default-runner" + echo "" + echo "3. Create systemd service:" + cat > /tmp/act_runner_service.txt << 'SERVICE' +[Unit] +Description=Forgejo Actions Runner +After=network.target + +[Service] +Type=simple +User=forgejo +WorkingDirectory=/var/lib/forgejo +ExecStart=/usr/local/bin/act_runner daemon +Restart=always +RestartSec=5 + +[Install] +WantedBy=multi-user.target +SERVICE + echo "Save this to /etc/systemd/system/act_runner.service" + cat /tmp/act_runner_service.txt + fi + + echo "" + echo "📊 Summary:" + echo " - Forgejo: $(systemctl is-active forgejo || echo 'not running')" + echo " - act_runner installed: $(command -v act_runner &> /dev/null && echo 'yes' || echo 'no')" + echo " - Runner service: $(systemctl is-active act_runner 2>/dev/null || echo 'not configured')" + + echo "" + echo "📝 Next steps:" + echo "1. Go to https://code.mnemo-cards.online/admin/actions/runners" + echo "2. Click 'Create new Runner'" + echo "3. Copy the registration token" + echo "4. Run this on the server:" + echo " act_runner register --no-interactive \\" + echo " --instance https://code.mnemo-cards.online \\" + echo " --token YOUR_TOKEN \\" + echo " --name default-runner" + echo "5. Enable and start the service:" + echo " systemctl enable act_runner" + echo " systemctl start act_runner" + +EOF + +echo "" +echo "✅ Forgejo Actions setup check completed!" +echo "" +echo "🌐 Open Forgejo: https://code.mnemo-cards.online" +echo "🔑 Login as: cinnabarflower" +echo "⚙️ Admin panel: https://code.mnemo-cards.online/admin/actions/runners" +