├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── dependabot.yaml └── workflows │ └── checkout.yml ├── .gitignore ├── .vscode ├── extensions.json ├── launch.json └── settings.json ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── LICENSE ├── Makefile ├── README.md ├── analysis_options.yaml ├── example ├── json_parser.dart └── math.dart ├── lib ├── isolation.dart └── src │ ├── channel.dart │ ├── compute.dart │ ├── connection.dart │ ├── connection_master.dart │ ├── connection_slave.dart │ ├── constant.dart │ ├── controller.dart │ ├── controller_base.dart │ ├── controller_master.dart │ ├── controller_slave.dart │ ├── entry_point.dart │ ├── event_queue.dart │ ├── exception.dart │ ├── handler.dart │ ├── logging.dart │ └── payload.dart ├── pubspec.yaml ├── test ├── isolation_test.dart └── unit │ ├── echo.dart │ └── pow.dart └── tool └── makefile ├── environment.mk ├── pub.mk └── test.mk /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "" 5 | labels: "" 6 | assignees: "" 7 | --- 8 | 9 | **Describe the bug** 10 | A clear and concise description of what the bug is. 11 | 12 | **To Reproduce** 13 | Steps to reproduce the behavior: 14 | 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | 28 | - OS: [e.g. iOS] 29 | - Browser [e.g. chrome, safari] 30 | - Version [e.g. 22] 31 | 32 | **Smartphone (please complete the following information):** 33 | 34 | - Device: [e.g. iPhone6] 35 | - OS: [e.g. iOS8.1] 36 | - Browser [e.g. stock browser, safari] 37 | - Version [e.g. 22] 38 | 39 | **Additional context** 40 | Add any other context about the problem here. 41 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "" 5 | labels: "" 6 | assignees: "" 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | 12 | **Describe the solution you'd like** 13 | A clear and concise description of what you want to happen. 14 | 15 | **Describe alternatives you've considered** 16 | A clear and concise description of any alternative solutions or features you've considered. 17 | 18 | **Additional context** 19 | Add any other context or screenshots about the feature request here. 20 | -------------------------------------------------------------------------------- /.github/dependabot.yaml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PlugFox/isolation/56c2adf0e22ffa2763b37a2db35c0edfedca764e/.github/dependabot.yaml -------------------------------------------------------------------------------- /.github/workflows/checkout.yml: -------------------------------------------------------------------------------- 1 | name: CHECKOUT 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - "master" 8 | - "develop" 9 | - "feature/**" 10 | - "bugfix/**" 11 | - "hotfix/**" 12 | - "support/**" 13 | paths: 14 | - "lib/**.dart" 15 | - "test/**.dart" 16 | - "example/**.dart" 17 | pull_request: 18 | branches: 19 | - "master" 20 | - "develop" 21 | - "feature/**" 22 | - "bugfix/**" 23 | - "hotfix/**" 24 | - "support/**" 25 | paths: 26 | - "lib/**.dart" 27 | - "test/**.dart" 28 | - "example/**.dart" 29 | 30 | jobs: 31 | checkout: 32 | name: "Checkout" 33 | runs-on: ubuntu-latest 34 | defaults: 35 | run: 36 | working-directory: ./ 37 | container: 38 | image: dart:stable 39 | timeout-minutes: 10 40 | steps: 41 | - name: 🚂 Get latest code 42 | uses: actions/checkout@v2 43 | 44 | - name: 🚃 Cache pub modules 45 | uses: actions/cache@v2 46 | env: 47 | cache-name: cache-leetcode-pub 48 | with: 49 | path: | 50 | $PWD/.pub_cache/ 51 | key: ${{ runner.os }}-leetcode 52 | 53 | - name: 🗄️ Export pub cache directory 54 | run: export PUB_CACHE=$PWD/.pub_cache/ 55 | 56 | - name: 👷 Install Dependencies 57 | timeout-minutes: 1 58 | run: dart pub get 59 | 60 | - name: 🔎 Check format 61 | timeout-minutes: 1 62 | run: dart format --set-exit-if-changed -l 80 -o none . 63 | 64 | - name: 📈 Check analyzer 65 | timeout-minutes: 1 66 | run: dart analyze --fatal-infos --fatal-warnings lib 67 | 68 | - name: 🧪 Run tests 69 | timeout-minutes: 2 70 | run: | 71 | dart run coverage:test_with_coverage -fb -o coverage -- \ 72 | --concurrency=6 --platform vm --coverage=./coverage --reporter=expanded test/isolation_test.dart 73 | 74 | - name: 📥 Upload coverage to Codecov 75 | timeout-minutes: 1 76 | uses: codecov/codecov-action@v2.1.0 77 | with: 78 | token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos 79 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Files and directories created by pub. 2 | .dart_tool/ 3 | .packages 4 | 5 | # Conventional directory for build outputs. 6 | build/ 7 | 8 | # Omit committing pubspec.lock for library packages; see 9 | # https://dart.dev/guides/libraries/private-files#pubspeclock. 10 | pubspec.lock 11 | 12 | coverage/ 13 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dart-code.dart-code" 4 | ] 5 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "isolation", 9 | "request": "launch", 10 | "type": "dart", 11 | "program": "example/isolation_example.dart", 12 | } 13 | ] 14 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[dart]": { 3 | "editor.defaultFormatter": "Dart-Code.dart-code", 4 | "editor.formatOnPaste": true, 5 | "editor.formatOnSave": true, 6 | "editor.insertSpaces": true 7 | }, 8 | "dart.lineLength": 80, 9 | "editor.rulers": [ 10 | 80 11 | ], 12 | "dart.doNotFormat": [] 13 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.0.2-dev 2 | 3 | - added: Initial proof of concept 4 | 5 | # 0.0.1-dev 6 | 7 | - added: placeholder 8 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | plugfox@gmail.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright © 2022 Plague Fox 2 | 3 | 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: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | 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. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: help 2 | 3 | help: 4 | @echo "Isolate helper for flutter and dart" 5 | @echo "" 6 | @dart --version 7 | 8 | -include tool/makefile/*.mk -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Isolation 2 | 3 | [![platform_info](https://img.shields.io/pub/v/isolation.svg)](https://pub.dev/packages/isolation) 4 | [![Actions Status](https://github.com/PlugFox/isolation/actions/workflows/checkout.yml/badge.svg)](https://github.com/PlugFox/isolation/actions/workflows/checkout.yml) 5 | [![Coverage](https://codecov.io/gh/PlugFox/isolation/branch/master/graph/badge.svg)](https://codecov.io/gh/PlugFox/isolation) 6 | [![License: MIT](https://img.shields.io/badge/license-MIT-purple.svg)](https://opensource.org/licenses/MIT) 7 | [![Linter](https://img.shields.io/badge/style-linter-40c4ff.svg)](https://dart-lang.github.io/linter/lints/) 8 | 9 | --- 10 | 11 | # Overview 12 | 13 | The package simplifies the creation and interaction between isolates. 14 | It encapsulates the entire boilerplate, leaving the developer with only transport with an API that looks like two stream controllers. 15 | 16 | The package also helps to preserve typing and pass exceptions between isolates. 17 | 18 | # Usage 19 | 20 | ## JSON parser 21 | 22 | ```dart 23 | import 'dart:async'; 24 | import 'dart:convert' show jsonDecode; 25 | 26 | import 'package:isolation/isolation.dart'; 27 | 28 | typedef JsonMap = Map; 29 | 30 | /// Main isolate 31 | void main() => Future(() async { 32 | // Create a new isolate controller 33 | final controller = IsolateController( 34 | _parser, // Isolate function 35 | lazy: true, // The isolate will not be created until the first message 36 | ) 37 | // Add few messages to the isolate: 38 | ..add('{}') 39 | ..add('{"field": 123}') 40 | ..add('{"fizz": "buzz", "value": 2, "undefined": null}'); 41 | // Listen messages from slave isolate 42 | await controller.stream.take(3).forEach(print); 43 | // Gracefully closing connection and finally kill slave isolate 44 | await controller.close(force: false); 45 | }); 46 | 47 | /// Slave isolate for parsing JSON, where you can subscribe to the stream 48 | /// from the main isolate and send the result back through the controller. 49 | Future _parser(IsolateController controller) => 50 | controller.stream.forEach((json) { 51 | final result = jsonDecode(json) as Object?; 52 | (result is JsonMap) 53 | ? controller.add(result) 54 | : controller.addError(const FormatException('Invalid JSON')); 55 | }); 56 | ``` 57 | 58 | ## Installation 59 | 60 | Add the following to your `pubspec.yaml` file to be able to do code generation: 61 | 62 | ```yaml 63 | dependencies: 64 | isolation: any 65 | ``` 66 | 67 | Then run: 68 | 69 | ```shell 70 | dart pub get 71 | ``` 72 | 73 | or 74 | 75 | ```shell 76 | flutter pub get 77 | ``` 78 | 79 | ## Coverage 80 | 81 | [![](https://codecov.io/gh/PlugFox/isolation/branch/master/graphs/sunburst.svg)](https://codecov.io/gh/PlugFox/isolation/branch/master) 82 | 83 | ## Changelog 84 | 85 | Refer to the [Changelog](https://github.com/plugfox/isolation/blob/master/CHANGELOG.md) to get all release notes. 86 | 87 | ## Maintainers 88 | 89 | [Plague Fox](https://plugfox.dev) 90 | 91 | ## License 92 | 93 | [MIT](https://github.com/plugfox/isolation/blob/master/LICENSE) 94 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | #include: package:lints/recommended.yaml 2 | 3 | analyzer: 4 | exclude: 5 | # Build 6 | - "build/**" 7 | # Tests 8 | - "test/**.mocks.dart" 9 | - ".test_coverage.dart" 10 | # Assets 11 | - "assets/**" 12 | 13 | strong-mode: 14 | implicit-casts: false 15 | implicit-dynamic: false 16 | 17 | errors: 18 | # Info 19 | todo: info 20 | directives_ordering: info 21 | always_declare_return_types: info 22 | 23 | # Warning 24 | unsafe_html: warning 25 | no_logic_in_create_state: warning 26 | empty_catches: warning 27 | 28 | # Error 29 | always_use_package_imports: error 30 | avoid_relative_lib_imports: error 31 | avoid_slow_async_io: error 32 | avoid_types_as_parameter_names: error 33 | cancel_subscriptions: error 34 | close_sinks: error 35 | valid_regexps: error 36 | always_require_non_null_named_parameters: error 37 | 38 | linter: 39 | rules: 40 | # Public packages 41 | public_member_api_docs: true 42 | lines_longer_than_80_chars: true 43 | 44 | # Flutter 45 | use_full_hex_values_for_flutter_colors: false 46 | flutter_style_todos: false 47 | sized_box_for_whitespace: false 48 | sized_box_shrink_expand: false 49 | use_decorated_box: false 50 | avoid_unnecessary_containers: false 51 | 52 | # Disabled 53 | avoid_annotating_with_dynamic: false 54 | unnecessary_final: false 55 | always_specify_types: false 56 | avoid_as: false 57 | avoid_redundant_argument_values: false 58 | comment_references: true # Unused because https://github.com/dart-lang/sdk/issues/36974 59 | prefer_double_quotes: false 60 | sort_constructors_first: false 61 | sort_unnamed_constructors_first: false 62 | avoid_web_libraries_in_flutter: false 63 | always_put_control_body_on_new_line: false 64 | sort_pub_dependencies: false 65 | do_not_use_environment: false 66 | diagnostic_describe_all_properties: false 67 | prefer_relative_imports: false 68 | depend_on_referenced_packages: false 69 | library_private_types_in_public_api: false 70 | prefer_final_parameters: false 71 | avoid_types_on_closure_parameters: false 72 | 73 | # Enabled 74 | always_use_package_imports: true 75 | use_named_constants: true 76 | use_is_even_rather_than_modulo: true 77 | unnecessary_await_in_return: true 78 | tighten_type_of_initializing_formals: true 79 | exhaustive_cases: true 80 | always_put_required_named_parameters_first: true 81 | avoid_bool_literals_in_conditional_expressions: true 82 | avoid_double_and_int_checks: true 83 | avoid_escaping_inner_quotes: true 84 | avoid_field_initializers_in_const_classes: true 85 | avoid_implementing_value_types: true 86 | avoid_js_rounded_ints: true 87 | avoid_print: true 88 | avoid_renaming_method_parameters: true 89 | avoid_returning_null_for_future: true 90 | avoid_returning_null_for_void: true 91 | avoid_single_cascade_in_expression_statements: true 92 | avoid_slow_async_io: true 93 | avoid_unused_constructor_parameters: true 94 | avoid_void_async: true 95 | await_only_futures: true 96 | cancel_subscriptions: true 97 | cascade_invocations: true 98 | close_sinks: true 99 | control_flow_in_finally: true 100 | empty_statements: true 101 | iterable_contains_unrelated_type: true 102 | join_return_with_assignment: true 103 | leading_newlines_in_multiline_strings: true 104 | list_remove_unrelated_type: true 105 | literal_only_boolean_expressions: true 106 | missing_whitespace_between_adjacent_strings: true 107 | no_adjacent_strings_in_list: true 108 | no_logic_in_create_state: true 109 | no_runtimeType_toString: true 110 | only_throw_errors: true 111 | overridden_fields: true 112 | package_names: true 113 | package_prefixed_library_names: true 114 | parameter_assignments: true 115 | prefer_asserts_in_initializer_lists: true 116 | prefer_asserts_with_message: true 117 | prefer_const_constructors: true 118 | prefer_const_constructors_in_immutables: true 119 | prefer_const_declarations: true 120 | prefer_const_literals_to_create_immutables: true 121 | prefer_constructors_over_static_methods: true 122 | prefer_expression_function_bodies: true 123 | prefer_final_in_for_each: true 124 | prefer_final_locals: true 125 | prefer_foreach: true 126 | prefer_if_elements_to_conditional_expressions: true 127 | prefer_inlined_adds: true 128 | prefer_int_literals: true 129 | prefer_is_not_operator: true 130 | prefer_null_aware_operators: true 131 | prefer_typing_uninitialized_variables: true 132 | prefer_void_to_null: true 133 | provide_deprecation_message: true 134 | sort_child_properties_last: true 135 | test_types_in_equals: true 136 | throw_in_finally: true 137 | unnecessary_null_aware_assignments: true 138 | unnecessary_overrides: true 139 | unnecessary_parenthesis: true 140 | unnecessary_raw_strings: true 141 | unnecessary_statements: true 142 | unnecessary_string_escapes: true 143 | unnecessary_string_interpolations: true 144 | unsafe_html: true 145 | use_raw_strings: true 146 | use_string_buffers: true 147 | valid_regexps: true 148 | void_checks: true 149 | always_declare_return_types: true 150 | always_require_non_null_named_parameters: true 151 | annotate_overrides: true 152 | avoid_empty_else: true 153 | avoid_init_to_null: true 154 | avoid_null_checks_in_equality_operators: true 155 | avoid_relative_lib_imports: true 156 | avoid_return_types_on_setters: true 157 | avoid_shadowing_type_parameters: true 158 | avoid_types_as_parameter_names: true 159 | camel_case_extensions: true 160 | curly_braces_in_flow_control_structures: true 161 | empty_catches: true 162 | empty_constructor_bodies: true 163 | library_names: true 164 | library_prefixes: true 165 | no_duplicate_case_values: true 166 | null_closures: true 167 | omit_local_variable_types: true 168 | prefer_adjacent_string_concatenation: true 169 | prefer_collection_literals: true 170 | prefer_conditional_assignment: true 171 | prefer_contains: true 172 | prefer_equal_for_default_values: true 173 | prefer_final_fields: true 174 | prefer_for_elements_to_map_fromIterable: true 175 | prefer_generic_function_type_aliases: true 176 | prefer_if_null_operators: true 177 | prefer_is_empty: true 178 | prefer_is_not_empty: true 179 | prefer_iterable_whereType: true 180 | prefer_single_quotes: true 181 | prefer_spread_collections: true 182 | recursive_getters: true 183 | slash_for_doc_comments: true 184 | type_init_formals: true 185 | unawaited_futures: true 186 | unnecessary_const: true 187 | unnecessary_new: true 188 | unnecessary_null_in_if_null_operators: true 189 | unnecessary_this: true 190 | unrelated_type_equality_checks: true 191 | use_function_type_syntax_for_parameters: true 192 | use_rethrow_when_possible: true 193 | camel_case_types: true 194 | file_names: true 195 | non_constant_identifier_names: true 196 | constant_identifier_names: true 197 | directives_ordering: true 198 | package_api_docs: true 199 | implementation_imports: true 200 | prefer_interpolation_to_compose_strings: true 201 | unnecessary_brace_in_string_interps: true 202 | avoid_function_literals_in_foreach_calls: true 203 | prefer_function_declarations_over_variables: true 204 | unnecessary_lambdas: true 205 | unnecessary_getters_setters: true 206 | prefer_initializing_formals: true 207 | avoid_catches_without_on_clauses: true 208 | avoid_catching_errors: true 209 | use_to_and_as_if_applicable: true 210 | one_member_abstracts: true 211 | avoid_classes_with_only_static_members: true 212 | prefer_mixin: true 213 | use_setters_to_change_properties: true 214 | avoid_setters_without_getters: true 215 | avoid_returning_null: true 216 | avoid_returning_this: true 217 | type_annotate_public_apis: true 218 | avoid_private_typedef_functions: true 219 | avoid_positional_boolean_parameters: true 220 | hash_and_equals: true 221 | avoid_equals_and_hash_code_on_mutable_classes: true 222 | avoid_dynamic_calls: true 223 | avoid_multiple_declarations_per_line: true 224 | avoid_type_to_string: true 225 | deprecated_consistency: true 226 | eol_at_end_of_file: true 227 | noop_primitive_operations: true 228 | prefer_null_aware_method_calls: true 229 | unnecessary_constructor_name: true 230 | use_if_null_to_convert_nulls_to_bools: true 231 | use_test_throws_matchers: true 232 | unnecessary_late: true 233 | conditional_uri_does_not_exist: true 234 | #avoid_final_parameters: true 235 | no_leading_underscores_for_library_prefixes: true 236 | no_leading_underscores_for_local_identifiers: true 237 | secure_pubspec_urls: true 238 | 239 | # Experimental 240 | use_late_for_private_fields_and_variables: true 241 | use_build_context_synchronously: true 242 | unnecessary_nullable_for_final_variable_declarations: true 243 | unnecessary_null_checks: true 244 | require_trailing_commas: true 245 | null_check_on_nullable_type_parameter: true 246 | cast_nullable_to_non_nullable: true 247 | invariant_booleans: true 248 | no_default_cases: false 249 | 250 | # Deprecated 251 | #avoid_as (deprecated) 252 | #prefer_bool_in_asserts (deprecated) 253 | #super_goes_last (deprecated) 254 | -------------------------------------------------------------------------------- /example/json_parser.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: avoid_print 2 | 3 | import 'dart:async'; 4 | import 'dart:convert' show jsonDecode; 5 | 6 | import 'package:isolation/isolation.dart'; 7 | 8 | typedef JsonMap = Map; 9 | 10 | void main() => Future(() async { 11 | final controller = IsolateController( 12 | _callback, 13 | lazy: true, 14 | ) 15 | ..add('{}') 16 | ..add('{"field": 123}') 17 | ..add('{"fizz": "buzz", "value": 2, "undefined": null}'); 18 | await controller.stream.take(3).forEach(print); 19 | await controller.close(force: false); 20 | }); 21 | 22 | Future _callback(IsolateController controller) => 23 | controller.stream.forEach((json) { 24 | final result = jsonDecode(json) as Object?; 25 | (result is JsonMap) 26 | ? controller.add(result) 27 | : controller.addError(const FormatException('Invalid JSON')); 28 | }); 29 | -------------------------------------------------------------------------------- /example/math.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: avoid_print 2 | 3 | import 'dart:async'; 4 | 5 | import 'package:isolation/isolation.dart'; 6 | 7 | void main() => Future(() async { 8 | final controller = IsolateController( 9 | _callback, 10 | lazy: true, 11 | ) 12 | ..add(2) 13 | ..add(4) 14 | ..add(8); 15 | await controller.stream.take(3).forEach((msg) => print('f> $msg')); 16 | await controller.close(force: false); 17 | }); 18 | 19 | Future _callback(IsolateController controller) => 20 | controller.stream.forEach((msg) { 21 | print('s> $msg'); 22 | controller.add('$msg ^ 2 = ${msg * msg}'); 23 | }); 24 | -------------------------------------------------------------------------------- /lib/isolation.dart: -------------------------------------------------------------------------------- 1 | library isolation; 2 | 3 | export 'src/compute.dart'; 4 | export 'src/controller.dart'; 5 | export 'src/handler.dart'; 6 | -------------------------------------------------------------------------------- /lib/src/channel.dart: -------------------------------------------------------------------------------- 1 | import 'dart:isolate'; 2 | 3 | import 'package:meta/meta.dart'; 4 | 5 | /// {@template isolate_channel} 6 | /// Channel between two isolates. 7 | /// Used to send data, exception or service messages between isolates. 8 | /// {@endtemplate} 9 | @internal 10 | class IsolateChannel extends Sink { 11 | /// {@macro isolate_channel} 12 | IsolateChannel([SendPort? sendPort]) 13 | : receivePort = ReceivePort(), 14 | _sendPort = sendPort; 15 | 16 | /// Isolate channel receive port already closed; 17 | bool get isClosed => _isClosed; 18 | bool _isClosed = false; 19 | 20 | /// Contain [SendPort] 21 | bool get hasSendPort => _sendPort != null; 22 | 23 | /// Allow receiving data from the isolate. 24 | final ReceivePort receivePort; 25 | 26 | /// Allow sending data to another isolate. 27 | SendPort? _sendPort; 28 | 29 | /// Set new send port 30 | // ignore: use_setters_to_change_properties 31 | void setPort(SendPort sendPort) => _sendPort = sendPort; 32 | 33 | @override 34 | void add(In data) { 35 | assert( 36 | hasSendPort, 37 | 'IsolateChannel is not connected to another isolate.', 38 | ); 39 | _sendPort?.send(data); 40 | } 41 | 42 | @override 43 | void close() { 44 | if (isClosed) return; 45 | receivePort.close(); 46 | _isClosed = true; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /lib/src/compute.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:developer'; 3 | import 'dart:isolate'; 4 | 5 | import 'package:meta/meta.dart'; 6 | 7 | /// Signature for the callback passed to [compute]. 8 | /// 9 | /// Instances of [ComputeCallback] must be functions that can be sent to an 10 | /// isolate. 11 | typedef ComputeCallback = FutureOr Function(Q message); 12 | 13 | /// The dart:io implementation of [compute]. 14 | @experimental 15 | Future compute( 16 | ComputeCallback callback, 17 | Q message, { 18 | String? debugLabel, 19 | }) async { 20 | debugLabel ??= const bool.fromEnvironment('dart.vm.product') 21 | ? 'compute' 22 | : callback.toString(); 23 | 24 | final flow = Flow.begin(); 25 | Timeline.startSync('$debugLabel: start', flow: flow); 26 | final port = RawReceivePort(); 27 | Timeline.finishSync(); 28 | 29 | void timeEndAndCleanup() { 30 | Timeline.startSync('$debugLabel: end', flow: Flow.end(flow.id)); 31 | port.close(); 32 | Timeline.finishSync(); 33 | } 34 | 35 | final completer = Completer(); 36 | port.handler = (Object? msg) { 37 | timeEndAndCleanup(); 38 | completer.complete(msg); 39 | }; 40 | 41 | try { 42 | await Isolate.spawn<_IsolateConfiguration>( 43 | _spawn, 44 | _IsolateConfiguration( 45 | callback, 46 | message, 47 | port.sendPort, 48 | debugLabel, 49 | flow.id, 50 | ), 51 | onExit: port.sendPort, 52 | onError: port.sendPort, 53 | debugName: debugLabel, 54 | ); 55 | } on Object { 56 | timeEndAndCleanup(); 57 | rethrow; 58 | } 59 | 60 | final response = await completer.future; 61 | if (response == null) { 62 | throw RemoteError('Isolate exited without result or error.', ''); 63 | } 64 | 65 | assert(response is List, 'Unexpected response type: $response'); 66 | response as List; 67 | 68 | final type = response.length; 69 | assert(1 <= type && type <= 3, 'Unexpected response type: $type'); 70 | 71 | switch (type) { 72 | // success; see _buildSuccessResponse 73 | case 1: 74 | return response[0] as R; 75 | 76 | // native error; see Isolate.addErrorListener 77 | case 2: 78 | await Future.error( 79 | RemoteError( 80 | response[0]! as String, 81 | response[1]! as String, 82 | ), 83 | ); 84 | 85 | // caught error; see _buildErrorResponse 86 | case 3: 87 | default: 88 | assert( 89 | type == 3 && response[2] == null, 90 | 'Unexpected response type', 91 | ); 92 | 93 | await Future.error( 94 | response[0]!, 95 | response[1]! as StackTrace, 96 | ); 97 | } 98 | } 99 | 100 | @immutable 101 | class _IsolateConfiguration { 102 | const _IsolateConfiguration( 103 | this.callback, 104 | this.message, 105 | this.resultPort, 106 | this.debugLabel, 107 | this.flowId, 108 | ); 109 | final ComputeCallback callback; 110 | final Q message; 111 | final SendPort resultPort; 112 | final String debugLabel; 113 | final int flowId; 114 | 115 | FutureOr applyAndTime() => Timeline.timeSync( 116 | debugLabel, 117 | () => callback(message), 118 | flow: Flow.step(flowId), 119 | ); 120 | } 121 | 122 | /// The spawn point MUST guarantee only one result event is sent through the 123 | /// [SendPort.send] be it directly or indirectly i.e. [Isolate.exit]. 124 | /// 125 | /// In case an [Error] or [Exception] are thrown AFTER the data 126 | /// is sent, they will NOT be handled or reported by the main [Isolate] because 127 | /// it stops listening after the first event is received. 128 | /// 129 | /// Also use the helpers [_buildSuccessResponse] and [_buildErrorResponse] to 130 | /// build the response 131 | Future _spawn(_IsolateConfiguration configuration) async { 132 | late final List computationResult; 133 | 134 | try { 135 | computationResult = 136 | _buildSuccessResponse(await configuration.applyAndTime()); 137 | } on Object catch (e, s) { 138 | computationResult = _buildErrorResponse(e, s); 139 | } 140 | 141 | Isolate.exit(configuration.resultPort, computationResult); 142 | } 143 | 144 | /// Wrap in [List] to ensure our expectations in the main [Isolate] are met. 145 | /// 146 | /// We need to wrap a success result in a [List] because the user provided type 147 | /// [R] could also be a [List]. Meaning, a check `result is R` could return true 148 | /// for what was an error event. 149 | List _buildSuccessResponse(R result) => List.filled(1, result); 150 | 151 | /// Wrap in [List] to ensure our expectations in the main isolate are met. 152 | /// 153 | /// We wrap a caught error in a 3 element [List]. Where the last element is 154 | /// always null. We do this so we have a way to know if an error was one we 155 | /// caught or one thrown by the library code. 156 | List _buildErrorResponse(Object error, StackTrace stack) => 157 | List.filled(3, null) 158 | ..[0] = error 159 | ..[1] = stack; 160 | -------------------------------------------------------------------------------- /lib/src/connection.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:isolation/src/channel.dart'; 4 | import 'package:isolation/src/exception.dart'; 5 | import 'package:isolation/src/logging.dart'; 6 | import 'package:meta/meta.dart'; 7 | 8 | /// {@template connection} 9 | /// Base class for master and slave isolate connections. 10 | /// {@endtemplate} 11 | @internal 12 | abstract class Connection implements EventSink { 13 | /// {@macro connection} 14 | Connection({ 15 | required this.dataChannel, 16 | required this.exceptionChannel, 17 | required this.serviceChannel, 18 | }); 19 | 20 | /// Channel for data 21 | @protected 22 | @nonVirtual 23 | final IsolateChannel dataChannel; 24 | 25 | /// Channel for exceptions 26 | @protected 27 | @nonVirtual 28 | final IsolateChannel exceptionChannel; 29 | 30 | /// Channel for service messages 31 | @protected 32 | @nonVirtual 33 | final IsolateChannel serviceChannel; 34 | 35 | /// Connection status 36 | ConnectionStatus get status => _status; 37 | ConnectionStatus _status = ConnectionStatus.notConnected; 38 | 39 | /// Establish connection with another isolate 40 | @mustCallSuper 41 | Future connect() async { 42 | _status = ConnectionStatus.connected; 43 | } 44 | 45 | @override 46 | @mustCallSuper 47 | void add(In data) { 48 | fine('Connection.add(${data.hashCode})'); 49 | assert(_status.isOpened, 'Connection is already closed.'); 50 | if (!_status.isOpened) return; 51 | dataChannel.add(data); 52 | } 53 | 54 | @override 55 | @mustCallSuper 56 | void addError(Object error, [StackTrace? stackTrace]) { 57 | fine('Connection.addError(${Error.safeToString(error)})'); 58 | assert(_status.isOpened, 'Connection is already closed.'); 59 | if (!_status.isOpened) return; 60 | exceptionChannel.add(IsolateException(error, stackTrace)); 61 | } 62 | 63 | /// Add service message 64 | @protected 65 | @mustCallSuper 66 | void addServiceMessage(Object? message) { 67 | assert(_status.isOpened, 'Connection is not opened.'); 68 | if (!_status.isOpened) return; 69 | serviceChannel.add(message); 70 | } 71 | 72 | @override 73 | @mustCallSuper 74 | FutureOr close({bool force = false}) { 75 | if (_status.isClosed) return null; 76 | _status = ConnectionStatus.disconnected; 77 | if (!force) { 78 | // ignore: todo 79 | // TODO: await graceful closing 80 | // Matiunin Mikhail , 07 August 2022 81 | //throw UnimplementedError('Connection.close(force: false)'); 82 | } 83 | dataChannel.close(); 84 | exceptionChannel.close(); 85 | serviceChannel.close(); 86 | } 87 | } 88 | 89 | /// {@nodoc} 90 | @internal 91 | enum ConnectionStatus { 92 | /// {@nodoc} 93 | notConnected('Connection is not established yet'), 94 | 95 | /// {@nodoc} 96 | connected('Connection is established'), 97 | 98 | /// {@nodoc} 99 | disconnected('Disconnected from isolate'); 100 | 101 | /// {@nodoc} 102 | const ConnectionStatus(this.message); 103 | 104 | /// {@nodoc} 105 | final String message; 106 | 107 | /// {@nodoc} 108 | bool get isNotConnected => this == notConnected; 109 | 110 | /// {@nodoc} 111 | bool get isOpened => this == connected; 112 | 113 | /// {@nodoc} 114 | bool get isClosed => this == disconnected; 115 | 116 | @override 117 | String toString() => Error.safeToString(this); 118 | } 119 | -------------------------------------------------------------------------------- /lib/src/connection_master.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:isolate'; 3 | 4 | import 'package:isolation/src/channel.dart'; 5 | import 'package:isolation/src/connection.dart'; 6 | import 'package:isolation/src/constant.dart'; 7 | import 'package:isolation/src/entry_point.dart'; 8 | import 'package:isolation/src/exception.dart'; 9 | import 'package:isolation/src/handler.dart'; 10 | import 'package:isolation/src/logging.dart'; 11 | import 'package:isolation/src/payload.dart'; 12 | import 'package:meta/meta.dart'; 13 | 14 | /// {@template master_connection} 15 | /// Isolate helper, that helps to create and manage another isolate. 16 | /// {@endtemplate} 17 | @internal 18 | class MasterConnection extends Connection { 19 | /// {@macro master_connection} 20 | MasterConnection({ 21 | required IsolateHandler handler, 22 | required EventSink out, 23 | String? debugName = 'MasterConnection', 24 | }) : _handler = handler, 25 | _eventsFromSlave = out, 26 | _debugName = debugName, 27 | super( 28 | dataChannel: IsolateChannel(), 29 | exceptionChannel: 30 | IsolateChannel(), 31 | serviceChannel: IsolateChannel(), 32 | ) { 33 | fine('$_debugName created'); 34 | } 35 | 36 | /// Slave isolation 37 | Isolate? _slaveIsolate; 38 | 39 | /// Handler for slave's events 40 | final EventSink _eventsFromSlave; 41 | 42 | /// Entry point 43 | final IsolateHandler _handler; 44 | 45 | /// Debug name 46 | final String? _debugName; 47 | 48 | /// Subscription for data from slave 49 | StreamSubscription? _dataSubscription; 50 | 51 | /// Subscription for errors from slave 52 | StreamSubscription? _exceptionSubscription; 53 | 54 | /// Subscription for service messages from slave 55 | StreamSubscription? _serviceSubscription; 56 | 57 | @override 58 | Future connect() async { 59 | assert(status.isNotConnected, '$_debugName has already been established.'); 60 | if (!status.isNotConnected) return; 61 | await super.connect(); 62 | fine('$_debugName starts connection'); 63 | // Payload for slave isolate. 64 | final payload = IsolatePayload( 65 | dataPort: super.dataChannel.receivePort.sendPort, 66 | exceptionPort: super.exceptionChannel.receivePort.sendPort, 67 | servicePort: super.serviceChannel.receivePort.sendPort, 68 | handler: _handler, 69 | errorsAreFatal: false, 70 | enableLogging: Zone.current[kLogEnabled] == true, 71 | ); 72 | final registrateListenersFuture = _registrateListeners(); 73 | _slaveIsolate = await Isolate.spawn>( 74 | isolateEntryPoint, 75 | payload, 76 | errorsAreFatal: payload.errorsAreFatal, 77 | onExit: payload.servicePort, 78 | debugName: _debugName, 79 | ).timeout(const Duration(milliseconds: 30000)); 80 | await registrateListenersFuture 81 | .timeout(const Duration(milliseconds: 30000)); 82 | } 83 | 84 | Future _registrateListeners() { 85 | fine('$_debugName start listening on data channel'); 86 | _dataSubscription = dataChannel.receivePort.cast().listen( 87 | _eventsFromSlave.add, 88 | onError: (Object error, StackTrace stackTrace) { 89 | warning( 90 | error, 91 | stackTrace, 92 | '$_debugName exception on data channel listener', 93 | ); 94 | _eventsFromSlave.addError(error, stackTrace); 95 | }, 96 | cancelOnError: false, 97 | ); 98 | 99 | fine('$_debugName start listening on exception channel'); 100 | _exceptionSubscription = 101 | exceptionChannel.receivePort.cast().listen( 102 | (msg) => _eventsFromSlave.addError(msg.exception, msg.stackTrace), 103 | onError: (Object error, StackTrace stackTrace) { 104 | warning( 105 | error, 106 | stackTrace, 107 | '$_debugName exception on exception channel listener', 108 | ); 109 | _eventsFromSlave.addError(error, stackTrace); 110 | }, 111 | cancelOnError: false, 112 | ); 113 | 114 | fine('$_debugName start listening on service channel'); 115 | final completer = Completer(); 116 | _serviceSubscription = serviceChannel.receivePort.cast().listen( 117 | (msg) { 118 | if (!completer.isCompleted) { 119 | if (msg is! List || msg.length != 3) { 120 | warning( 121 | 'Instead SendPorts received unexpected message', 122 | ); 123 | throw UnsupportedError('Unexpected message'); 124 | } 125 | fine('$_debugName recive send ports from SlaveConnection'); 126 | super.dataChannel.setPort(msg[0]); 127 | super.exceptionChannel.setPort(msg[1]); 128 | super.serviceChannel.setPort(msg[2]); 129 | completer.complete(); 130 | } 131 | }, 132 | ); 133 | return completer.future; 134 | } 135 | 136 | @override 137 | Future close({bool force = false}) async { 138 | try { 139 | config('$_debugName is closing'); 140 | addServiceMessage(null); 141 | if (!force) { 142 | // ignore: todo 143 | // TODO: await response from slave isolate. 144 | //_slaveIsolate.addOnExitListener(responsePort) 145 | } 146 | super.close(force: force); 147 | await _dataSubscription?.cancel(); 148 | await _exceptionSubscription?.cancel(); 149 | await _serviceSubscription?.cancel(); 150 | } finally { 151 | _slaveIsolate?.kill(); 152 | config('$_debugName is closed'); 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /lib/src/connection_slave.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:isolate'; 3 | 4 | import 'package:isolation/src/channel.dart'; 5 | import 'package:isolation/src/connection.dart'; 6 | import 'package:isolation/src/exception.dart'; 7 | import 'package:isolation/src/logging.dart'; 8 | import 'package:meta/meta.dart'; 9 | 10 | /// {@template slave_connection} 11 | /// Isolate helper, that helps to create and manage another isolate. 12 | /// {@endtemplate} 13 | @internal 14 | class SlaveConnection extends Connection { 15 | /// {@macro slave_isolate_connection} 16 | SlaveConnection({ 17 | required SendPort dataSendPort, 18 | required SendPort exceptiondataSendPort, 19 | required SendPort servicedataSendPort, 20 | required EventSink out, 21 | String? debugName = 'SlaveConnection', 22 | }) : _debugName = debugName, 23 | _eventsFromMaster = out, 24 | super( 25 | dataChannel: IsolateChannel( 26 | dataSendPort, 27 | ), 28 | exceptionChannel: IsolateChannel( 29 | exceptiondataSendPort, 30 | ), 31 | serviceChannel: IsolateChannel( 32 | servicedataSendPort, 33 | ), 34 | ) { 35 | fine('$_debugName created'); 36 | } 37 | 38 | /// Combine data and exception from master isolate 39 | final EventSink _eventsFromMaster; 40 | 41 | StreamSubscription? _dataSubscription; 42 | StreamSubscription? _exceptionSubscription; 43 | StreamSubscription? _serviceSubscription; 44 | 45 | /// Debug name 46 | final String? _debugName; 47 | 48 | @override 49 | Future connect() async { 50 | assert(status.isNotConnected, '$_debugName has already been established.'); 51 | if (!status.isNotConnected) return; 52 | fine('$_debugName connection is started'); 53 | await super.connect(); 54 | _registrateListeners(); 55 | addServiceMessage( 56 | [ 57 | super.dataChannel.receivePort.sendPort, 58 | super.exceptionChannel.receivePort.sendPort, 59 | super.serviceChannel.receivePort.sendPort, 60 | ], 61 | ); 62 | } 63 | 64 | void _registrateListeners() { 65 | fine('$_debugName start listening on data channel'); 66 | _dataSubscription = dataChannel.receivePort.cast().listen( 67 | _eventsFromMaster.add, 68 | onError: (Object error, StackTrace stackTrace) { 69 | warning( 70 | error, 71 | stackTrace, 72 | '$_debugName exception on data channel listener', 73 | ); 74 | _eventsFromMaster.addError(error, stackTrace); 75 | }, 76 | cancelOnError: false, 77 | ); 78 | 79 | fine('$_debugName start listening on exception channel'); 80 | _exceptionSubscription = 81 | exceptionChannel.receivePort.cast().listen( 82 | (msg) => _eventsFromMaster.addError(msg.exception, msg.stackTrace), 83 | onError: (Object error, StackTrace stackTrace) { 84 | warning( 85 | error, 86 | stackTrace, 87 | '$_debugName exception on exception channel listener', 88 | ); 89 | _eventsFromMaster.addError(error, stackTrace); 90 | }, 91 | cancelOnError: false, 92 | ); 93 | 94 | fine('$_debugName start listening on service channel'); 95 | _serviceSubscription = serviceChannel.receivePort.cast().listen( 96 | (msg) { 97 | if (msg == null) { 98 | close(); 99 | return; 100 | } 101 | }, 102 | onError: (Object error, StackTrace stackTrace) { 103 | warning( 104 | error, 105 | stackTrace, 106 | '$_debugName exception on exception service listener', 107 | ); 108 | _eventsFromMaster.addError(error, stackTrace); 109 | }, 110 | cancelOnError: false, 111 | ); 112 | } 113 | 114 | @override 115 | Future close({bool force = false}) async { 116 | try { 117 | config('$_debugName is closing'); 118 | addServiceMessage(null); 119 | super.close(); 120 | _dataSubscription?.cancel().ignore(); 121 | _exceptionSubscription?.cancel().ignore(); 122 | _serviceSubscription?.cancel().ignore(); 123 | if (!force) { 124 | // ignore: todo 125 | // TODO: await all data and exception before kill 126 | } 127 | } finally { 128 | config('$_debugName is closed'); 129 | Isolate.current.kill(); 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /lib/src/constant.dart: -------------------------------------------------------------------------------- 1 | @internal 2 | 3 | import 'package:meta/meta.dart'; 4 | 5 | /// Enable logging key 6 | const Symbol kLogEnabled = #isolation.log.enabled; 7 | -------------------------------------------------------------------------------- /lib/src/controller.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:isolation/src/controller_master.dart'; 4 | import 'package:isolation/src/handler.dart'; 5 | 6 | /// {@template isolate_controller} 7 | /// Isolate controller. 8 | /// {@endtemplate} 9 | abstract class IsolateController implements EventSink { 10 | /// {@macro isolate_controller} 11 | factory IsolateController( 12 | IsolateHandler handler, { 13 | bool lazy, 14 | String? debugLabel, 15 | }) = IsolateControllerMasterImpl; 16 | 17 | /// Output data & error stream 18 | Stream get stream; 19 | 20 | /// Whether the stream controller is closed for adding more events. 21 | /// 22 | /// The controller becomes closed by calling the [close] method. 23 | /// New events cannot be added, by calling [add] or [addError], 24 | /// to a closed controller. 25 | /// 26 | /// If the controller is closed, 27 | /// the "done" event might not have been delivered yet, 28 | /// but it has been scheduled, and it is too late to add more events. 29 | bool get isClosed; 30 | 31 | /// Closes the sink. 32 | /// 33 | /// Calling this method more than once is allowed, but does nothing. 34 | /// 35 | /// Neither [add] nor [addError] must be called after this method. 36 | @override 37 | Future close({bool force = false}); 38 | } 39 | -------------------------------------------------------------------------------- /lib/src/controller_base.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:isolation/src/connection.dart'; 4 | import 'package:isolation/src/controller.dart'; 5 | import 'package:isolation/src/event_queue.dart'; 6 | import 'package:isolation/src/logging.dart'; 7 | import 'package:meta/meta.dart'; 8 | 9 | /// {@nodoc} 10 | @internal 11 | abstract class IsolateControllerBase 12 | implements IsolateController { 13 | /// {@nodoc} 14 | IsolateControllerBase({ 15 | this.debugLabel, 16 | }) : eventQueue = EventQueue( 17 | debugLabel: debugLabel == null ? null : 'EventQueue#$debugLabel', 18 | ); 19 | 20 | /// {@nodoc} 21 | @protected 22 | abstract final Connection connection; 23 | 24 | /// {@nodoc} 25 | @protected 26 | @nonVirtual 27 | final EventQueue eventQueue; 28 | 29 | /// {@nodoc} 30 | @protected 31 | @nonVirtual 32 | final String? debugLabel; 33 | 34 | /// {@nodoc} 35 | @protected 36 | @nonVirtual 37 | late final StreamController outputController = 38 | StreamController.broadcast(); 39 | 40 | @override 41 | @nonVirtual 42 | bool get isClosed => _isClosed; 43 | bool _isClosed = false; 44 | 45 | @override 46 | @useResult 47 | @nonVirtual 48 | Stream get stream => outputController.stream; 49 | 50 | @override 51 | @nonVirtual 52 | void add(Input event) { 53 | if (_isClosed) return; 54 | if (connection.status.isNotConnected) { 55 | eventQueue.add(() { 56 | assert(!_isClosed, 'Cannot be added to a closed controller'); 57 | if (_isClosed) return Future.value(); 58 | return connection.connect(); 59 | }); 60 | } 61 | eventQueue.add(() { 62 | assert(!_isClosed, 'Cannot be added to a closed controller'); 63 | if (_isClosed) return; 64 | connection.add(event); 65 | }); 66 | } 67 | 68 | @override 69 | @nonVirtual 70 | void addError(Object error, [StackTrace? stackTrace]) { 71 | if (_isClosed) return; 72 | eventQueue.add(() { 73 | assert(!_isClosed, 'Cannot be added to a closed controller'); 74 | connection.addError(error, stackTrace); 75 | }); 76 | } 77 | 78 | @override 79 | @mustCallSuper 80 | Future close({bool force = false}) async { 81 | if (_isClosed) return; 82 | if (force) { 83 | config('$debugLabel closing'); 84 | await eventQueue.close(force: true); 85 | await connection.close(force: true); 86 | await outputController.close(); 87 | _isClosed = true; 88 | } else { 89 | eventQueue 90 | // ignore: unawaited_futures 91 | ..add(() => config('$debugLabel closing')) 92 | // ignore: unawaited_futures 93 | ..add(connection.close) 94 | // ignore: unawaited_futures 95 | ..add(outputController.close) 96 | // ignore: unawaited_futures 97 | ..add(() => _isClosed = true) 98 | // ignore: unawaited_futures 99 | ..add(eventQueue.close); 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /lib/src/controller_master.dart: -------------------------------------------------------------------------------- 1 | import 'package:isolation/src/connection.dart'; 2 | import 'package:isolation/src/connection_master.dart'; 3 | import 'package:isolation/src/controller_base.dart'; 4 | import 'package:isolation/src/handler.dart'; 5 | import 'package:meta/meta.dart'; 6 | 7 | /// {@nodoc} 8 | @internal 9 | class IsolateControllerMasterImpl 10 | extends IsolateControllerBase { 11 | /// {@nodoc} 12 | IsolateControllerMasterImpl( 13 | IsolateHandler handler, { 14 | bool lazy = true, 15 | super.debugLabel = 'IsolateControllerMaster', 16 | }) { 17 | connection = MasterConnection( 18 | handler: handler, 19 | out: outputController, 20 | debugName: debugLabel, 21 | ); 22 | if (!lazy) { 23 | eventQueue.add(connection.connect); 24 | } 25 | } 26 | 27 | @override 28 | // ignore: close_sinks 29 | late final Connection connection; 30 | } 31 | -------------------------------------------------------------------------------- /lib/src/controller_slave.dart: -------------------------------------------------------------------------------- 1 | import 'dart:isolate'; 2 | 3 | import 'package:isolation/src/connection.dart'; 4 | import 'package:isolation/src/connection_slave.dart'; 5 | import 'package:isolation/src/controller_base.dart'; 6 | import 'package:meta/meta.dart'; 7 | 8 | /// {@nodoc} 9 | @internal 10 | class IsolateControllerSlaveImpl 11 | extends IsolateControllerBase { 12 | /// {@nodoc} 13 | IsolateControllerSlaveImpl({ 14 | required SendPort dataSendPort, 15 | required SendPort exceptiondataSendPort, 16 | required SendPort servicedataSendPort, 17 | super.debugLabel = 'IsolateControllerSlave', 18 | }) { 19 | connection = SlaveConnection( 20 | dataSendPort: dataSendPort, 21 | exceptiondataSendPort: exceptiondataSendPort, 22 | servicedataSendPort: servicedataSendPort, 23 | out: outputController, 24 | ); 25 | } 26 | 27 | @override 28 | // ignore: close_sinks 29 | late final Connection connection; 30 | } 31 | -------------------------------------------------------------------------------- /lib/src/entry_point.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:isolate'; 3 | 4 | import 'package:isolation/src/connection.dart'; 5 | import 'package:isolation/src/constant.dart'; 6 | import 'package:isolation/src/exception.dart'; 7 | import 'package:isolation/src/logging.dart'; 8 | import 'package:isolation/src/payload.dart'; 9 | import 'package:meta/meta.dart'; 10 | 11 | /// Entry point for the isolate. 12 | @internal 13 | void isolateEntryPoint(IsolatePayload payload) { 14 | Connection? connection; 15 | runZonedGuarded( 16 | () async { 17 | info('Execute entry payload in slave isolate'); 18 | connection = await payload(); 19 | }, 20 | (error, stackTrace) { 21 | severe(error, stackTrace, 'Root exception in slave isolate is catched'); 22 | payload.exceptionPort.send( 23 | IsolateException(error, stackTrace), 24 | ); 25 | if (payload.errorsAreFatal) { 26 | info('Closing slave isolate after fatal error'); 27 | connection?.close(); 28 | Isolate.current.kill(); 29 | } 30 | }, 31 | zoneValues: { 32 | kLogEnabled: payload.enableLogging, 33 | }, 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /lib/src/event_queue.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:collection'; 3 | import 'dart:developer' as developer; 4 | 5 | import 'package:isolation/src/logging.dart'; 6 | import 'package:meta/meta.dart'; 7 | 8 | /// Async callback 9 | typedef EventCallback = FutureOr Function(); 10 | 11 | /// {@template event_queue} 12 | /// An event queue is a queue of [EventCallback]s that are executed in order. 13 | /// {@endtemplate} 14 | class EventQueue implements Sink { 15 | /// {@macro event_queue} 16 | EventQueue({String? debugLabel}) { 17 | _debugLabel = debugLabel ?? 18 | (const bool.fromEnvironment('dart.vm.product') 19 | ? 'EventQueue' 20 | : 'EventQueue#$hashCode'); 21 | } 22 | 23 | /// Is the queue closed? 24 | bool get isClosed => _closed; 25 | bool _closed = false; 26 | final Queue<_EventQueueTask> _queue = Queue<_EventQueueTask>(); 27 | late final String? _debugLabel; 28 | Future? _processing; 29 | 30 | @override 31 | Future add(EventCallback event) { 32 | if (_closed) { 33 | throw StateError('EventQueue is closed'); 34 | } 35 | final task = _EventQueueTask(event); 36 | _queue.add(task); 37 | developer.Timeline.instantSync('$_debugLabel:add'); 38 | _start().ignore(); 39 | return task.future; 40 | } 41 | 42 | @override 43 | Future close({bool force = false}) async { 44 | _closed = true; 45 | if (force) { 46 | _queue.clear(); 47 | } else { 48 | await _processing; 49 | } 50 | } 51 | 52 | Future _start() { 53 | final processing = _processing; 54 | if (processing != null) { 55 | return processing; 56 | } 57 | final flow = developer.Flow.begin(); 58 | developer.Timeline.instantSync('$_debugLabel:begin'); 59 | return _processing = Future.doWhile(() async { 60 | if (_queue.isEmpty) { 61 | _processing = null; 62 | developer.Timeline.instantSync('$_debugLabel:end'); 63 | developer.Flow.end(flow.id); 64 | return false; 65 | } 66 | try { 67 | await developer.Timeline.timeSync( 68 | '$_debugLabel:task', 69 | _queue.removeFirst(), 70 | flow: developer.Flow.step(flow.id), 71 | ); 72 | } on Object catch (error, stackTrace) { 73 | warning(error, stackTrace, '$_debugLabel:exception'); 74 | } 75 | return true; 76 | }); 77 | } 78 | } 79 | 80 | @immutable 81 | class _EventQueueTask { 82 | _EventQueueTask(EventCallback event) 83 | : _fn = event, 84 | _completer = Completer(); 85 | 86 | final EventCallback _fn; 87 | final Completer _completer; 88 | 89 | Future get future => _completer.future; 90 | 91 | Future call() async { 92 | try { 93 | await _fn(); 94 | _completer.complete(); 95 | } on Object catch (error, stackTrace) { 96 | _completer.completeError(error, stackTrace); 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /lib/src/exception.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | /// {@template connection_exception} 4 | /// Wrapper for exceptions to pass between isolates 5 | /// {@endtemplate} 6 | @internal 7 | @immutable 8 | class IsolateException implements Exception { 9 | /// {@macro connection_exception} 10 | IsolateException(this.exception, [StackTrace? stackTrace]) 11 | : _stackTraceString = stackTrace?.toString(); 12 | 13 | /// Exception 14 | final Object exception; 15 | 16 | /// Stack trace 17 | StackTrace? get stackTrace => _stackTraceString == null 18 | ? null 19 | : StackTrace.fromString(_stackTraceString!); 20 | 21 | final String? _stackTraceString; 22 | } 23 | -------------------------------------------------------------------------------- /lib/src/handler.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:isolation/src/controller.dart'; 4 | 5 | /// An event handler is responsible for reacting to an incoming [Input] 6 | /// and can emit zero or more [Output] via the [controller]'s sink. 7 | /// 8 | /// ```dart 9 | /// void handler(IsolateController slaveController) => 10 | /// slaveController.stream.listen((i) => 11 | /// slaveController.sink.add('$i + $i = ${i * 2}') 12 | /// ); 13 | /// 14 | /// final masterController = IsolateController(handler); 15 | /// masterController.stream.listen(print); // prints: "2 + 2 = 4" 16 | /// masterController.add(2); 17 | /// masterController.close(); 18 | /// ``` 19 | typedef IsolateHandler = FutureOr Function( 20 | IsolateController controller, 21 | ); 22 | -------------------------------------------------------------------------------- /lib/src/logging.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:developer' as developer; 3 | 4 | import 'package:isolation/src/constant.dart'; 5 | 6 | /// Tracing information 7 | final void Function(Object? message) fine = _logAll('FINE', 500); 8 | 9 | /// Static configuration messages 10 | final void Function(Object? message) config = _logAll('CONF', 700); 11 | 12 | /// Iformational messages 13 | final void Function(Object? message) info = _logAll('INFO', 800); 14 | 15 | /// Potential problems 16 | final void Function(Object exception, [StackTrace? stackTrace, String? reason]) 17 | warning = _logAll('WARN', 900); 18 | 19 | /// Serious failures 20 | final void Function(Object error, [StackTrace stackTrace, String? reason]) 21 | severe = _logAll('ERR!', 1000); 22 | 23 | void Function( 24 | Object? message, [ 25 | StackTrace? stackTrace, 26 | String? reason, 27 | ]) _logAll(String prefix, int level) => 28 | (Object? message, [StackTrace? stackTrace, String? reason]) { 29 | if (Zone.current[kLogEnabled] != true) return; 30 | developer.log( 31 | '[$prefix] ${reason ?? message}', 32 | level: level, 33 | name: 'isolation', 34 | error: message is Exception || message is Error ? message : null, 35 | stackTrace: stackTrace, 36 | ); 37 | }; 38 | -------------------------------------------------------------------------------- /lib/src/payload.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:isolate'; 3 | 4 | import 'package:isolation/src/connection.dart'; 5 | import 'package:isolation/src/controller_slave.dart'; 6 | import 'package:isolation/src/handler.dart'; 7 | import 'package:meta/meta.dart'; 8 | 9 | /// {@template isolate_payload} 10 | /// IsolatePayload class 11 | /// {@endtemplate} 12 | @internal 13 | @immutable 14 | class IsolatePayload { 15 | /// {@macro isolate_payload} 16 | const IsolatePayload({ 17 | required this.dataPort, 18 | required this.exceptionPort, 19 | required this.servicePort, 20 | required this.handler, 21 | required this.enableLogging, 22 | required this.errorsAreFatal, 23 | }); 24 | 25 | /// Isolate payload data port 26 | final SendPort dataPort; 27 | 28 | /// Isolate payload exception port 29 | final SendPort exceptionPort; 30 | 31 | /// Isolate payload service port 32 | final SendPort servicePort; 33 | 34 | /// Handler 35 | final IsolateHandler handler; 36 | 37 | /// Enable logging 38 | final bool enableLogging; 39 | 40 | /// Sets whether uncaught errors will terminate the isolate. 41 | final bool errorsAreFatal; 42 | 43 | /// Finish slave isolate initialization 44 | Future> call() async { 45 | try { 46 | // ignore: close_sinks 47 | final controller = IsolateControllerSlaveImpl( 48 | dataSendPort: dataPort, 49 | exceptiondataSendPort: exceptionPort, 50 | servicedataSendPort: servicePort, 51 | ); 52 | await controller.connection.connect(); 53 | // Workaround with handler to save types between isolates: 54 | Future(() => handler(controller)).ignore(); 55 | return controller.connection; 56 | } on Object { 57 | rethrow; 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: isolation 2 | description: | 3 | The library allows you to simplify the creation and interaction of isolates for dart and flutter. 4 | The interaction between isolates is an interface similar to stream controllers. 5 | version: 0.0.2-dev 6 | repository: https://github.com/PlugFox/isolation 7 | issue_tracker: https://github.com/PlugFox/isolation/issues 8 | homepage: https://github.com/PlugFox/isolation 9 | 10 | environment: 11 | sdk: ">=2.17.0 <3.0.0" 12 | 13 | dependencies: 14 | meta: ^1.7.0 15 | 16 | dev_dependencies: 17 | test: ^1.16.0 18 | coverage: any 19 | -------------------------------------------------------------------------------- /test/isolation_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:isolation/src/constant.dart'; 4 | 5 | import 'unit/echo.dart' as echo; 6 | import 'unit/pow.dart' as pow; 7 | 8 | void main() => runZoned( 9 | () { 10 | echo.main(); 11 | pow.main(); 12 | }, 13 | zoneValues: {kLogEnabled: true}, 14 | ); 15 | -------------------------------------------------------------------------------- /test/unit/echo.dart: -------------------------------------------------------------------------------- 1 | @Timeout(Duration(seconds: 5)) 2 | 3 | import 'package:isolation/isolation.dart'; 4 | import 'package:test/test.dart'; 5 | 6 | void main() => group( 7 | 'echo', 8 | () { 9 | late IsolateController controller; 10 | 11 | setUpAll(() { 12 | controller = IsolateController(_echo); 13 | }); 14 | 15 | tearDownAll(() { 16 | controller.close(); 17 | }); 18 | 19 | test('single', () { 20 | controller.add('ping'); 21 | expect( 22 | controller.stream.first, 23 | completion(equals('ping')), 24 | ); 25 | }); 26 | 27 | test('double', () { 28 | controller 29 | ..add('1') 30 | ..add('2'); 31 | expectLater( 32 | controller.stream.take(2), 33 | emitsInOrder(['1', '2']), 34 | ); 35 | }); 36 | }, 37 | timeout: const Timeout(Duration(seconds: 5)), 38 | ); 39 | 40 | Future _echo(IsolateController controller) => 41 | controller.stream.forEach((event) => controller.add(event)); 42 | -------------------------------------------------------------------------------- /test/unit/pow.dart: -------------------------------------------------------------------------------- 1 | @Timeout(Duration(seconds: 5)) 2 | 3 | import 'dart:async'; 4 | 5 | import 'package:isolation/isolation.dart'; 6 | import 'package:test/test.dart'; 7 | 8 | void main() => group( 9 | 'pow', 10 | () { 11 | test('single', () async { 12 | final controller = IsolateController(_pow2); 13 | await expectLater( 14 | (controller..add(4)).stream.first, 15 | completion(equals('16')), 16 | ); 17 | await controller.close(); 18 | }); 19 | 20 | test('double', () async { 21 | final controller = IsolateController(_pow2) 22 | ..add(1) 23 | ..add(2); 24 | await expectLater( 25 | controller.stream.take(2), 26 | emitsInOrder(['1', '4']), 27 | ); 28 | await controller.close(); 29 | }); 30 | }, 31 | timeout: const Timeout(Duration(seconds: 5)), 32 | ); 33 | 34 | Future _pow2(IsolateController controller) => 35 | controller.stream.forEach((e) => controller.add('${e * e}')); 36 | -------------------------------------------------------------------------------- /tool/makefile/environment.mk: -------------------------------------------------------------------------------- 1 | 2 | PLATFORM := $(shell arch) 3 | ifeq ($(PLATFORM),x86_64) 4 | GLIBC := assets/glibc/x86_64/glibc-2.29-r0.apk 5 | GLIBC_BIN := assets/glibc/x86_64/glibc-bin-2.29-r0.apk 6 | else ifeq ($(PLATFORM),arm64) 7 | GLIBC := assets/glibc/arm/glibc-2.30-r0.apk 8 | GLIBC_BIN := assets/glibc/arm/glibc-bin-2.30-r0.apk 9 | else 10 | GLIBC := assets/glibc/x86_64/glibc-2.29-r0.apk 11 | GLIBC_BIN := assets/glibc/x86_64/glibc-bin-2.29-r0.apk 12 | endif 13 | 14 | ifeq ($(OS),Windows_NT) 15 | OS := win 16 | else 17 | _detected_OS := $(shell uname -s) 18 | ifeq ($(_detected_OS),Linux) 19 | OS := lin 20 | else ifeq ($(_detected_OS),Darwin) 21 | OS := mac 22 | endif 23 | endif 24 | -------------------------------------------------------------------------------- /tool/makefile/pub.mk: -------------------------------------------------------------------------------- 1 | .PHONY: get 2 | 3 | get: 4 | @dart pub get 5 | 6 | pana: 7 | @dart pub global activate pana 8 | @dart pub global run pana 9 | 10 | deploy: 11 | @dart pub publish -------------------------------------------------------------------------------- /tool/makefile/test.mk: -------------------------------------------------------------------------------- 1 | .PHONY: test 2 | 3 | test: get 4 | @dart run coverage:test_with_coverage -fb -o coverage -- \ 5 | --concurrency=6 --coverage=./coverage --reporter=expanded test/isolation_test.dart --------------------------------------------------------------------------------