├── .gitignore ├── lib ├── src │ ├── utils │ │ ├── utils_web.dart │ │ ├── io_utils.dart │ │ └── utils.dart │ ├── services │ │ ├── isolate_executor_service.dart │ │ └── executors │ │ │ ├── isolate_executor_web.dart │ │ │ └── isolate_executor.dart │ ├── exceptions.dart │ ├── tasks │ │ ├── task_tracker.dart │ │ ├── task_event.dart │ │ ├── task_output.dart │ │ └── tasks.dart │ └── task_manager.dart └── executorservices.dart ├── pubspec.yaml ├── analysis_options.yaml ├── example ├── utils.dart └── example.dart ├── CHANGELOG.md ├── LICENSE ├── test ├── test_utils.dart ├── task_manager_test.dart ├── isolate_executor_web_tests.dart ├── task_manager_task_outputs_test.dart └── executors_test.dart └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Files and directories created by pub 2 | .dart_tool/ 3 | .packages 4 | # Remove the following pattern if you wish to check in your lock file 5 | pubspec.lock 6 | 7 | # Conventional directory for build outputs 8 | build/ 9 | 10 | # Directory created by dartdoc 11 | doc/api/ 12 | -------------------------------------------------------------------------------- /lib/src/utils/utils_web.dart: -------------------------------------------------------------------------------- 1 | /// todo: update check when type aliases will be available in dart. 2 | Object createTaskIdentifier() => Object(); 3 | 4 | /// Get the cpu count on the machine. 5 | /// 6 | /// Note: since it's might not be available on web, i think 4 is a good number. 7 | int getCpuCount() => 4; 8 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: executorservices 2 | description: A Dart package providing services that allow you to execute dart code in different executors, ex - isolates. 3 | version: 2.0.3 4 | homepage: https://github.com/bitsydarel/executorservices 5 | author: darelbitsy 6 | 7 | environment: 8 | sdk: '>=2.4.0 <3.0.0' 9 | 10 | dependencies: 11 | meta: ^1.1.7 12 | 13 | dev_dependencies: 14 | flutter_code_style: ^1.6.5 15 | test: ^1.9.3 16 | mockito: ^4.1.1 17 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # Defines a default set of lint rules enforced for 2 | # projects at Google. For details and rationale, 3 | # see https://github.com/dart-lang/pedantic#enabled-lints. 4 | include: package:flutter_code_style/analysis_options.yaml 5 | 6 | # For lint rules and documentation, see http://dart-lang.github.io/linter/lints. 7 | # Uncomment to specify additional rules. 8 | # linter: 9 | # rules: 10 | # - camel_case_types 11 | 12 | analyzer: 13 | # exclude: 14 | # - path/to/excluded/files/** 15 | -------------------------------------------------------------------------------- /lib/src/utils/io_utils.dart: -------------------------------------------------------------------------------- 1 | import "dart:io"; 2 | import "dart:isolate"; 3 | 4 | /// Hack for creating something similar to type alias. 5 | /// And use [Capability]. 6 | /// 7 | /// We could use something like: 8 | /// 9 | /// ``` 10 | /// mixin _TaskIdentifierMarker {} 11 | /// 12 | /// class TaskIdentifier = Capability with _TaskIdentifierMarker; 13 | /// ``` 14 | /// 15 | /// But since [Capability] does not have 16 | /// an unnamed constructor it's not possible. 17 | /// 18 | /// todo: update check when type aliases will be available in dart. 19 | Object createTaskIdentifier() => Capability(); 20 | 21 | /// Get the cpu count on the machine. 22 | int getCpuCount() => Platform.numberOfProcessors; 23 | -------------------------------------------------------------------------------- /example/utils.dart: -------------------------------------------------------------------------------- 1 | import "dart:isolate"; 2 | import "dart:math"; 3 | 4 | Future concurrentFunction(final int id) async { 5 | printRunningIsolate("concurrentFunction:$id"); 6 | final randomNumber = Random.secure().nextInt(10); 7 | await Future.delayed(Duration(seconds: randomNumber)); 8 | return randomNumber; 9 | } 10 | 11 | Future concurrentFunctionTimed( 12 | final int id, 13 | final DateTime startTime, 14 | ) async { 15 | printRunningIsolate("concurrentFunction:$id"); 16 | final randomNumber = Random.secure().nextInt(10); 17 | await Future.delayed(Duration(seconds: randomNumber)); 18 | print("Elapsed time: ${DateTime.now().difference(startTime)}"); 19 | return randomNumber; 20 | } 21 | 22 | void printRunningIsolate(final String tag) { 23 | print("Running $tag in isolate: ${Isolate.current.debugName}"); 24 | } 25 | -------------------------------------------------------------------------------- /lib/src/services/isolate_executor_service.dart: -------------------------------------------------------------------------------- 1 | import "package:executorservices/executorservices.dart"; 2 | import "package:executorservices/src/tasks/task_output.dart"; 3 | import "executors/isolate_executor_web.dart" 4 | if (dart.library.io) "executors/isolate_executor.dart"; 5 | 6 | /// A [IsolateExecutorService] that run [Task] 7 | /// into a [IsolateExecutor]. 8 | class IsolateExecutorService extends ExecutorService { 9 | /// Create a [IsolateExecutorService] with the following [identifier]. 10 | /// 11 | /// [maxConcurrency] is the max of isolate to use for executing [Task]. 12 | /// 13 | /// [allowCleanup] if true we will kill unused isolate 14 | /// if the [maxConcurrency] is greater than 5. 15 | IsolateExecutorService( 16 | String identifier, 17 | int maxConcurrency, { 18 | bool allowCleanup = false, 19 | }) : super( 20 | identifier, 21 | maxConcurrency, 22 | releaseUnusedExecutors: allowCleanup, 23 | ); 24 | 25 | int _isolateCounter = 1; 26 | 27 | @override 28 | Executor createExecutor(final OnTaskCompleted onTaskCompleted) { 29 | return IsolateExecutor( 30 | "${identifier}_executor${_isolateCounter++}", 31 | onTaskCompleted, 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lib/src/exceptions.dart: -------------------------------------------------------------------------------- 1 | import "../executorservices.dart"; 2 | 3 | /// A [Exception] that's thrown when a [ExecutorService] 4 | /// is shutting down but client keep submitting work to it. 5 | class TaskRejectedException implements Exception { 6 | /// Create a [TaskRejectedException] with the following [service]. 7 | const TaskRejectedException(this.service); 8 | 9 | /// [ExecutorService] that trowed the [TaskRejectedException]. 10 | final ExecutorService service; 11 | 12 | @override 13 | String toString() => "Task can't be submitted because " 14 | "[${service.runtimeType}:${service.identifier}]" 15 | " is shutting down"; 16 | } 17 | 18 | /// A [Exception] that's thrown when a [Task] failed with a exception. 19 | /// 20 | /// Why having a custom [Exception] ? 21 | /// 22 | /// Because in platform supporting isolate sendPort send method 23 | /// does not accept object that are not from the same code and in the same 24 | /// process unless they are primitive. 25 | class TaskFailedException implements Exception { 26 | /// [TaskFailedException] for the following [errorType] and [errorMessage]. 27 | const TaskFailedException(this.errorType, this.errorMessage); 28 | 29 | /// The type of the error that was thrown. 30 | final Type errorType; 31 | 32 | /// Message describing the error. 33 | final String errorMessage; 34 | 35 | @override 36 | String toString() => "Task failed with $errorType because $errorMessage"; 37 | } 38 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.0.0 2 | 3 | - Initial version, with support for Isolate executor service. 4 | 5 | ## 1.0.1 6 | 7 | - Updated the documentation. 8 | 9 | ## 1.0.2 10 | 11 | - Added support for functions that does not return a future. 12 | 13 | ## 1.0.3 14 | 15 | - Added support for functions that does not return a future on WEB also. 16 | 17 | ## 1.0.4 18 | 19 | - Updated the readme to demonstrate the new feature, updated the task classes to support to return FutoreOr. 20 | 21 | ## 1.0.5 22 | 23 | - Added shutdown for last oldest unused isolate to free memory gradually. 24 | 25 | ## 1.0.6 26 | 27 | - Added possibility to re-submit the same instance of a task to an ExecutorService. 28 | 29 | ## 2.0.0 30 | 31 | - Added subscribable task feature. 32 | 33 | - Subscribable tasks can emit many values before completing. 34 | 35 | - Subscribable task's result is a stream, which mean that you can pause, resume and cancel it. 36 | 37 | ## 2.0.0+1 38 | 39 | - Added example for subscribable tasks. 40 | 41 | ## 2.0.1 42 | 43 | - Changed LICENSE from GPL 3 to BSD-3 to allow more adoption. 44 | 45 | ## 2.0.2 46 | 47 | - Cleaner documentation for each methods provided by the library 48 | 49 | ## 2.0.2+1 50 | 51 | - Removed dependency on meta 1.1.8 to allow integration in old flutter projects. 52 | 53 | ## 2.0.3 54 | 55 | - Delegated SubscribableTask pause, resume, cancel hooks to the task manager. 56 | - Add ability to cancel a SubscribableTask before execution. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2019, Bitsy Darel 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /lib/src/utils/utils.dart: -------------------------------------------------------------------------------- 1 | import "dart:async"; 2 | 3 | /// todo: update check when type aliases will be available in dart. 4 | Object createTaskIdentifier() => throw UnsupportedError( 5 | "Cannot create a task identifier without dart:html or dart:io.", 6 | ); 7 | 8 | /// Get the cpu count on the machine. 9 | int getCpuCount() => throw UnsupportedError( 10 | "Cannot get cpu count without dart:html or dart:io.", 11 | ); 12 | 13 | /// Cleanup a ongoing subscription to a [StreamSubscription]. 14 | Future cleanupSubscription( 15 | final Object taskIdentifier, 16 | final Map inProgressSubscriptions, 17 | ) async { 18 | final inProgressTask = inProgressSubscriptions.remove(taskIdentifier); 19 | 20 | if (inProgressTask != null) { 21 | // cancel the in progress subscribable task subscription. 22 | await inProgressTask.cancel(); 23 | } else { 24 | print("subscribable task cleanup requested but the task is not running"); 25 | } 26 | } 27 | 28 | /// Pause a ongoing subscription to a [StreamSubscription]. 29 | void pauseSubscription( 30 | final Object taskIdentifier, 31 | final Map inProgressSubscriptions, 32 | ) { 33 | // ignore: cancel_subscriptions 34 | final inProgressTask = inProgressSubscriptions[taskIdentifier]; 35 | 36 | if (inProgressTask != null) { 37 | // pause the in progress subscribable task subscription. 38 | inProgressTask.pause(); 39 | } else { 40 | print("subscribable task pause requested but the task is not running"); 41 | } 42 | } 43 | 44 | /// Resume a ongoing subscription to a [StreamSubscription]. 45 | void resumeSubscription( 46 | final Object taskIdentifier, 47 | final Map inProgressSubscriptions, 48 | ) { 49 | // ignore: cancel_subscriptions 50 | final inProgressTask = inProgressSubscriptions[taskIdentifier]; 51 | 52 | if (inProgressTask != null) { 53 | // resume the in progress subscribable task subscription. 54 | inProgressTask.resume(); 55 | } else { 56 | print("subscribable task resume requested but the task is not running"); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /test/test_utils.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import "dart:math"; 3 | 4 | import "package:executorservices/executorservices.dart"; 5 | import 'package:executorservices/src/tasks/task_output.dart'; 6 | import "package:mockito/mockito.dart"; 7 | 8 | class MockExecutor extends Mock implements Executor { 9 | MockExecutor(this.id); 10 | 11 | final int id; 12 | 13 | @override 14 | String toString() => "MockExecutor$id"; 15 | } 16 | 17 | class CleanableFakeDelayingTask extends FakeDelayingTask { 18 | CleanableFakeDelayingTask([Duration delay]) : super(delay); 19 | 20 | @override 21 | CleanableFakeDelayingTask clone() => 22 | CleanableFakeDelayingTask(delaySimulationTime); 23 | } 24 | 25 | class FakeDelayingTask extends Task { 26 | FakeDelayingTask([Duration delay]) 27 | : delaySimulationTime = delay ?? const Duration(seconds: 1); 28 | 29 | final Duration delaySimulationTime; 30 | 31 | @override 32 | Future execute() async { 33 | await Future.delayed(delaySimulationTime); 34 | return Random.secure().nextInt(delaySimulationTime.inMilliseconds); 35 | } 36 | } 37 | 38 | class FakeFailureTask extends Task { 39 | FakeFailureTask(this.failPoint); 40 | 41 | final int failPoint; 42 | 43 | @override 44 | FutureOr execute() async { 45 | for (var index = 0; index < failPoint * 2; index++) { 46 | await Future.delayed(const Duration(milliseconds: 100)); 47 | assert(index != failPoint, "Fail point reached"); 48 | } 49 | return failPoint; 50 | } 51 | } 52 | 53 | class FakeSubscribableTask extends SubscribableTask { 54 | FakeSubscribableTask(this.max); 55 | 56 | final int max; 57 | 58 | @override 59 | Stream execute() async* { 60 | for (var index = 0; index < max; index++) { 61 | await Future.delayed(const Duration(milliseconds: 100)); 62 | yield index; 63 | } 64 | } 65 | } 66 | 67 | class FakeFailureSubscribableTask extends SubscribableTask { 68 | FakeFailureSubscribableTask(this.failPoint); 69 | 70 | final int failPoint; 71 | 72 | @override 73 | Stream execute() async* { 74 | for (var index = 0; index < failPoint * 2; index++) { 75 | await Future.delayed(const Duration(milliseconds: 100)); 76 | assert(index != failPoint, "Fail point reached"); 77 | yield index; 78 | } 79 | } 80 | } 81 | 82 | class MockOnTaskCompleted extends Mock { 83 | void call(TaskOutput output, Executor executor); 84 | } 85 | -------------------------------------------------------------------------------- /lib/src/tasks/task_tracker.dart: -------------------------------------------------------------------------------- 1 | import "dart:async"; 2 | 3 | import "package:executorservices/executorservices.dart"; 4 | 5 | /// Base class for define a the result of a [BaseTask]. 6 | /// 7 | /// Note: the [BaseTaskTracker] is not bounded to the [BaseTask] id 8 | /// because we support task cloning so the completer need to be reusable. 9 | abstract class BaseTaskTracker { 10 | /// Get the object allowing to follow the progress of a [BaseTask]. 11 | R progress(); 12 | 13 | /// Complete the [BaseTask] with [exception]. 14 | void completeWithError(TaskFailedException exception); 15 | } 16 | 17 | /// A [BaseTaskTracker] subclass that follow process of a [Task]. 18 | class TaskTracker extends BaseTaskTracker> { 19 | final Completer _completer = Completer(); 20 | 21 | @override 22 | Future progress() => _completer.future; 23 | 24 | /// Complete the [Task] with the [result]. 25 | void complete(final R result) => _completer.complete(result); 26 | 27 | @override 28 | void completeWithError(final TaskFailedException exception) { 29 | _completer.completeError(exception); 30 | } 31 | } 32 | 33 | /// A [BaseTaskTracker] subclass that allow 34 | /// to follow process of a [SubscribableTask]. 35 | class SubscribableTaskTracker extends BaseTaskTracker> { 36 | /// Create [SubscribableTaskTracker]. 37 | SubscribableTaskTracker() : _streamController = StreamController(); 38 | 39 | final StreamController _streamController; 40 | 41 | @override 42 | Stream progress() => _streamController.stream; 43 | 44 | /// Add a new [event] to the [SubscribableTask]. 45 | void addEvent(final R event) => _streamController.add(event); 46 | 47 | // ignore: use_setters_to_change_properties 48 | /// Set cancellation callback to [onCancelCallback]. 49 | void setCancellationCallback(void Function() onCancelCallback) { 50 | _streamController.onCancel = onCancelCallback; 51 | } 52 | 53 | // ignore: use_setters_to_change_properties 54 | /// Set the on pause callback to [onPauseCallback]. 55 | void setPauseCallback(void Function() onPauseCallback) { 56 | _streamController.onPause = onPauseCallback; 57 | } 58 | 59 | // ignore: use_setters_to_change_properties 60 | /// Set the on pause callback to [onResumeCallback]. 61 | void setResumeCallback(void Function() onResumeCallback) { 62 | _streamController.onResume = onResumeCallback; 63 | } 64 | 65 | /// Complete the [SubscribableTask]. 66 | void complete() async { 67 | // because the close method also call the cancel 68 | // method to cancel any ongoing subscription 69 | // we need to set it to null to avoid recalling the onCancel. 70 | _streamController.onCancel = null; 71 | return await _streamController.close(); 72 | } 73 | 74 | @override 75 | void completeWithError(TaskFailedException exception) { 76 | _streamController.addError(exception); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /lib/src/tasks/task_event.dart: -------------------------------------------------------------------------------- 1 | import "package:executorservices/executorservices.dart"; 2 | 3 | /// Event that notify that a new [BaseTask] have been submitted. 4 | class SubmittedTaskEvent { 5 | /// Create a [SubmittedTaskEvent] for [task] and [executor]. 6 | SubmittedTaskEvent(this.task, this.executor); 7 | 8 | /// Task to be executed. 9 | final BaseTask task; 10 | 11 | /// Executor to executed the [task]. 12 | /// 13 | /// Might be null if there's no available [Executor] to execute the [task]. 14 | Executor executor; 15 | 16 | @override 17 | bool operator ==(Object other) => 18 | identical(this, other) || 19 | other is SubmittedTaskEvent && 20 | runtimeType == other.runtimeType && 21 | task == other.task && 22 | executor == other.executor; 23 | 24 | @override 25 | int get hashCode => task.hashCode ^ executor.hashCode; 26 | 27 | @override 28 | String toString() => "SubmittedTaskEvent{task: $task, executor: $executor}"; 29 | } 30 | 31 | /// Event that notify that a [SubscribableTask] should be cancelled. 32 | class CancelledSubscribableTaskEvent { 33 | /// Create a [CancelledSubscribableTaskEvent] for [taskIdentifier]. 34 | CancelledSubscribableTaskEvent(this.taskIdentifier); 35 | 36 | /// Task's unique identifier. 37 | final Object taskIdentifier; 38 | 39 | @override 40 | bool operator ==(Object other) => 41 | identical(this, other) || 42 | other is CancelledSubscribableTaskEvent && 43 | runtimeType == other.runtimeType && 44 | taskIdentifier == other.taskIdentifier; 45 | 46 | @override 47 | int get hashCode => taskIdentifier.hashCode; 48 | } 49 | 50 | /// Event that notify that a [SubscribableTask] should be paused. 51 | class PauseSubscribableTaskEvent { 52 | /// Create a [PauseSubscribableTaskEvent] for [taskIdentifier]. 53 | PauseSubscribableTaskEvent(this.taskIdentifier); 54 | 55 | /// Task's unique identifier. 56 | final Object taskIdentifier; 57 | 58 | @override 59 | bool operator ==(Object other) => 60 | identical(this, other) || 61 | other is PauseSubscribableTaskEvent && 62 | runtimeType == other.runtimeType && 63 | taskIdentifier == other.taskIdentifier; 64 | 65 | @override 66 | int get hashCode => taskIdentifier.hashCode; 67 | } 68 | 69 | /// Event that notify that a [SubscribableTask] should be resumed. 70 | class ResumeSubscribableTaskEvent { 71 | /// Create a [ResumeSubscribableTaskEvent] for [taskIdentifier]. 72 | ResumeSubscribableTaskEvent(this.taskIdentifier); 73 | 74 | /// Task's unique identifier. 75 | final Object taskIdentifier; 76 | 77 | @override 78 | bool operator ==(Object other) => 79 | identical(this, other) || 80 | other is ResumeSubscribableTaskEvent && 81 | runtimeType == other.runtimeType && 82 | taskIdentifier == other.taskIdentifier; 83 | 84 | @override 85 | int get hashCode => taskIdentifier.hashCode; 86 | } 87 | -------------------------------------------------------------------------------- /example/example.dart: -------------------------------------------------------------------------------- 1 | import "dart:async"; 2 | import "dart:math"; 3 | 4 | import "package:executorservices/executorservices.dart"; 5 | import "package:http/http.dart" as http; 6 | 7 | import "utils.dart"; 8 | 9 | void main() { 10 | final executorService = ExecutorService.newUnboundExecutor(); 11 | 12 | executorService 13 | .submit( 14 | GetJsonFromUrlTask("https://jsonplaceholder.typicode.com/posts/1"), 15 | ) 16 | .then((data) => printRunningIsolate("GetJsonFromUrlTask:result\n$data")); 17 | 18 | executorService 19 | .submitAction(onShotFunction) 20 | .then((_) => printRunningIsolate("onShotFunction:done")); 21 | 22 | executorService.submitCallable(getRandomNumber, 10).then( 23 | (number) => printRunningIsolate( 24 | "getRandomNumber:result:$number", 25 | ), 26 | ); 27 | 28 | executorService.submitCallable(getRandomNumberSync, 1000000).then( 29 | (number) => printRunningIsolate( 30 | "getRandomNumberSync:result:$number", 31 | ), 32 | ); 33 | 34 | executorService 35 | .submitFunction2(getFullName, "Darel", "Bitsy") 36 | .then((result) => printRunningIsolate("getFullName:result:$result")); 37 | 38 | executorService 39 | .submitFunction3(greet, "Darel", "Bitsy", "bdeg") 40 | .then((result) => printRunningIsolate("greet:result:$result")); 41 | 42 | executorService.subscribeToCallable(getPosts, 10).listen( 43 | (number) => print("event received: $number"), 44 | onError: (error) => print("error received $error"), 45 | onDone: () => print("task is done"), 46 | ); 47 | 48 | executorService 49 | .subscribeToCallable(getPosts, 10) 50 | .asyncMap( 51 | (post) => executorService.submitCallable(getRandomNumber, post.length), 52 | ) 53 | .asyncMap( 54 | (number) => 55 | executorService.submitCallable(getRandomNumberSync, number + 1), 56 | ) 57 | .listen( 58 | (number) => print("event received: $number"), 59 | onError: (error) => print("error received $error"), 60 | onDone: () => print("task is done"), 61 | ); 62 | } 63 | 64 | Stream getPosts(final int max) async* { 65 | for (var index = 0; index < max; index++) { 66 | final post = await http.get( 67 | "https://jsonplaceholder.typicode.com/posts/$index", 68 | ); 69 | 70 | yield post.body; 71 | } 72 | } 73 | 74 | void onShotFunction() async { 75 | printRunningIsolate("onShotFunction:enter"); 76 | await Future.delayed(Duration(seconds: 3)); 77 | } 78 | 79 | Future getRandomNumber(final int max) async { 80 | printRunningIsolate("getRandomNumber:enter"); 81 | await Future.delayed(Duration(seconds: 2)); 82 | return Random.secure().nextInt(max); 83 | } 84 | 85 | int getRandomNumberSync(final int max) { 86 | printRunningIsolate("getRandomNumber:enter"); 87 | return Random.secure().nextInt(max); 88 | } 89 | 90 | Future getFullName(final String firstName, final String lastName) { 91 | printRunningIsolate("getRandomNumber"); 92 | return Future.delayed(Duration(seconds: 1)) 93 | .then((_) => "$firstName $lastName"); 94 | } 95 | 96 | Future greet( 97 | final String firstName, 98 | final String lastName, 99 | final String surname, 100 | ) { 101 | printRunningIsolate("greet"); 102 | return Future.delayed(Duration(seconds: 2)) 103 | .then((_) => "Hello $firstName $lastName $surname"); 104 | } 105 | 106 | class GetJsonFromUrlTask extends Task { 107 | GetJsonFromUrlTask(this.url); 108 | 109 | final String url; 110 | 111 | @override 112 | FutureOr execute() { 113 | return http.get(url).then((response) { 114 | printRunningIsolate(url); 115 | return response.body; 116 | }); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /lib/src/services/executors/isolate_executor_web.dart: -------------------------------------------------------------------------------- 1 | import "dart:async"; 2 | 3 | import "package:executorservices/executorservices.dart"; 4 | import "package:executorservices/src/tasks/task_event.dart"; 5 | import "package:executorservices/src/tasks/task_output.dart"; 6 | import "package:executorservices/src/utils/utils.dart"; 7 | 8 | /// [Executor] that execute [Task] into a isolate. 9 | class IsolateExecutor extends Executor { 10 | /// Create an [IsolateExecutor] with the specified [identifier]. 11 | IsolateExecutor( 12 | this.identifier, 13 | OnTaskCompleted taskCompletion, 14 | ) : assert(taskCompletion != null, "taskCompletion can't be null"), 15 | super(taskCompletion); 16 | 17 | /// The identifier of the [IsolateExecutor]. 18 | final String identifier; 19 | 20 | var _processingTask = false; 21 | 22 | DateTime _lastUsage; 23 | 24 | final _inProgressSubscriptions = {}; 25 | 26 | @override 27 | void execute(BaseTask task) { 28 | _processingTask = true; 29 | 30 | if (task is Task) { 31 | Future.microtask(() async { 32 | try { 33 | final intermediary = (task as Task).execute(); 34 | 35 | _setTerminalTaskOutput( 36 | SuccessTaskOutput( 37 | task.identifier, 38 | intermediary is Future ? await intermediary : intermediary, 39 | ), 40 | ); 41 | } on Object catch (error) { 42 | final taskError = TaskFailedException( 43 | error.runtimeType, 44 | error.toString(), 45 | ); 46 | 47 | _setTerminalTaskOutput(FailedTaskOutput(task.identifier, taskError)); 48 | } 49 | }); 50 | } else if (task is SubscribableTask) { 51 | try { 52 | final stream = (task as SubscribableTask).execute(); 53 | 54 | // Ignoring the cancellation of the subscription 55 | // because it's the user who control when to cancel the stream. 56 | // We also cancel stream when it's stream is done. 57 | // ignore: cancel_subscriptions 58 | final subscription = stream.listen( 59 | (event) { 60 | _lastUsage = DateTime.now(); 61 | 62 | onTaskCompleted( 63 | SubscribableTaskEvent(task.identifier, event), 64 | this, 65 | ); 66 | }, 67 | onError: (error) { 68 | _lastUsage = DateTime.now(); 69 | 70 | onTaskCompleted( 71 | SubscribableTaskError( 72 | task.identifier, 73 | TaskFailedException(error.runtimeType, error.toString()), 74 | ), 75 | this, 76 | ); 77 | }, 78 | onDone: () async { 79 | await cleanupSubscription( 80 | task.identifier, 81 | _inProgressSubscriptions, 82 | ); 83 | 84 | _setTerminalTaskOutput(SubscribableTaskDone(task.identifier)); 85 | }, 86 | ); 87 | // keep the subscription so that it's can be cancelled, paused, resumed. 88 | _inProgressSubscriptions[task.identifier] = subscription; 89 | } on Object catch (error) { 90 | final taskError = TaskFailedException( 91 | error.runtimeType, 92 | error.toString(), 93 | ); 94 | 95 | _setTerminalTaskOutput(FailedTaskOutput(task.identifier, taskError)); 96 | } 97 | } else { 98 | throw ArgumentError( 99 | "${task.runtimeType} messages are not supported," 100 | " your message should extends $Task or $SubscribableTask", 101 | ); 102 | } 103 | 104 | _lastUsage = DateTime.now(); 105 | } 106 | 107 | @override 108 | void cancelSubscribableTask(CancelledSubscribableTaskEvent event) async { 109 | await cleanupSubscription(event.taskIdentifier, _inProgressSubscriptions); 110 | _setTerminalTaskOutput(SubscribableTaskCancelled(event.taskIdentifier)); 111 | } 112 | 113 | @override 114 | void pauseSubscribableTask(PauseSubscribableTaskEvent event) { 115 | pauseSubscription(event.taskIdentifier, _inProgressSubscriptions); 116 | } 117 | 118 | @override 119 | void resumeSubscribableTask(ResumeSubscribableTaskEvent event) { 120 | resumeSubscription(event.taskIdentifier, _inProgressSubscriptions); 121 | _lastUsage = DateTime.now(); 122 | } 123 | 124 | @override 125 | bool isBusy() => _processingTask; 126 | 127 | @override 128 | FutureOr kill() async { 129 | for (final subscription in _inProgressSubscriptions.values) { 130 | await subscription.cancel(); 131 | } 132 | 133 | _inProgressSubscriptions.clear(); 134 | } 135 | 136 | @override 137 | DateTime lastUsage() => _lastUsage; 138 | 139 | void _setTerminalTaskOutput(final TaskOutput output) { 140 | _processingTask = false; 141 | onTaskCompleted(output, this); 142 | _lastUsage = DateTime.now(); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /lib/src/tasks/task_output.dart: -------------------------------------------------------------------------------- 1 | import "package:executorservices/executorservices.dart"; 2 | 3 | typedef OnTaskCompleted = void Function(TaskOutput output, Executor executor); 4 | 5 | /// Class representing a [Task]'s output. 6 | abstract class TaskOutput { 7 | /// Create [TaskOutput] for a [Task] with [taskIdentifier]. 8 | const TaskOutput(this.taskIdentifier); 9 | 10 | /// Identifier that identify the [TaskOutput] a specific [Task]. 11 | final Object taskIdentifier; 12 | 13 | @override 14 | bool operator ==(Object other) => 15 | identical(this, other) || 16 | other is TaskOutput && 17 | runtimeType == other.runtimeType && 18 | taskIdentifier == other.taskIdentifier; 19 | 20 | @override 21 | int get hashCode => taskIdentifier.hashCode; 22 | } 23 | 24 | /// Subclass of [TaskOutput] that represent a failure output. 25 | class FailedTaskOutput extends TaskOutput { 26 | /// Create [SuccessTaskOutput] for a [Task] that failed with [error]. 27 | const FailedTaskOutput(Object taskIdentifier, this.error) 28 | : assert(taskIdentifier != null, "taskIdentifier can't be null"), 29 | assert(error != null, "error can't be null"), 30 | super(taskIdentifier); 31 | 32 | /// The error produced by a [Task]. 33 | final TaskFailedException error; 34 | 35 | @override 36 | bool operator ==(Object other) => 37 | identical(this, other) || 38 | super == other && 39 | other is FailedTaskOutput && 40 | runtimeType == other.runtimeType && 41 | error == other.error; 42 | 43 | @override 44 | int get hashCode => super.hashCode ^ error.hashCode; 45 | 46 | @override 47 | String toString() => "FailedTaskOutput{error: $error}"; 48 | } 49 | 50 | /// Subclass of [TaskOutput] that represent a success output. 51 | class SuccessTaskOutput extends TaskOutput { 52 | /// Create [SuccessTaskOutput] for a [Task] that succeeded with [result]. 53 | const SuccessTaskOutput(Object taskIdentifier, this.result) 54 | : assert(taskIdentifier != null, "taskIdentifier can't be null"), 55 | super(taskIdentifier); 56 | 57 | /// The result of a [Task]. 58 | final R result; 59 | 60 | @override 61 | bool operator ==(Object other) => 62 | identical(this, other) || 63 | super == other && 64 | other is SuccessTaskOutput && 65 | runtimeType == other.runtimeType && 66 | result == other.result; 67 | 68 | @override 69 | int get hashCode => super.hashCode ^ result.hashCode; 70 | 71 | @override 72 | String toString() => "SuccessTaskOutput{result: $result}"; 73 | } 74 | 75 | /// Subclass of [TaskOutput] that represent a stream event output. 76 | class SubscribableTaskEvent extends TaskOutput { 77 | /// Create [SubscribableTaskEvent] for a 78 | /// [SubscribableTask] that emitted a [event]. 79 | const SubscribableTaskEvent(Object taskIdentifier, this.event) 80 | : assert(taskIdentifier != null, "taskIdentifier can't be null"), 81 | super(taskIdentifier); 82 | 83 | /// The event emitted by a [SubscribableTask]. 84 | final R event; 85 | 86 | @override 87 | bool operator ==(Object other) => 88 | identical(this, other) || 89 | super == other && 90 | other is SubscribableTaskEvent && 91 | runtimeType == other.runtimeType && 92 | event == other.event; 93 | 94 | @override 95 | int get hashCode => super.hashCode ^ event.hashCode; 96 | } 97 | 98 | /// Subclass of [TaskOutput] that represent a stream error output. 99 | class SubscribableTaskError extends TaskOutput { 100 | /// Create [SubscribableTaskError] for a 101 | /// [SubscribableTask] that emitted a [error]. 102 | const SubscribableTaskError(Object taskIdentifier, this.error) 103 | : assert(taskIdentifier != null, "taskIdentifier can't be null"), 104 | super(taskIdentifier); 105 | 106 | /// The error emitted by a [SubscribableTask]. 107 | final TaskFailedException error; 108 | 109 | @override 110 | bool operator ==(Object other) => 111 | identical(this, other) || 112 | super == other && 113 | other is SubscribableTaskError && 114 | runtimeType == other.runtimeType && 115 | error == other.error; 116 | 117 | @override 118 | int get hashCode => super.hashCode ^ error.hashCode; 119 | } 120 | 121 | /// Subclass of [TaskOutput] that represent a [SubscribableTask] cancellation. 122 | class SubscribableTaskCancelled extends TaskOutput { 123 | /// Create [SubscribableTaskCancelled] for a 124 | /// [SubscribableTask] that is done emitting events. 125 | SubscribableTaskCancelled(Object taskIdentifier) : super(taskIdentifier); 126 | } 127 | 128 | /// Subclass of [TaskOutput] that represent a [SubscribableTask] completion. 129 | class SubscribableTaskDone extends TaskOutput { 130 | /// Create [SubscribableTaskDone] for a 131 | /// [SubscribableTask] that has been cancelled. 132 | const SubscribableTaskDone(Object taskIdentifier) : super(taskIdentifier); 133 | } 134 | -------------------------------------------------------------------------------- /test/task_manager_test.dart: -------------------------------------------------------------------------------- 1 | import "dart:async"; 2 | import "dart:collection"; 3 | 4 | import "package:executorservices/src/task_manager.dart"; 5 | import "package:executorservices/src/tasks/task_event.dart"; 6 | import "package:executorservices/src/tasks/task_tracker.dart"; 7 | import "package:mockito/mockito.dart"; 8 | import "package:test/test.dart"; 9 | 10 | import "test_utils.dart"; 11 | 12 | void main() { 13 | test( 14 | "Should throw a error if the task manager " 15 | "contains a task and the task can't be cloned", 16 | () { 17 | final task = FakeDelayingTask(Duration(milliseconds: 500)); 18 | final tracker = SubscribableTaskTracker(); 19 | 20 | final taskManager = TaskManager.private( 21 | {task.identifier: tracker}, 22 | Queue(), 23 | StreamController(), 24 | null, 25 | ); 26 | 27 | expect( 28 | () => taskManager.handle(TaskRequest(task, tracker)), 29 | throwsUnsupportedError, 30 | ); 31 | 32 | expect(taskManager.getInProgressTasks(), hasLength(1)); 33 | }, 34 | ); 35 | 36 | test( 37 | "Should add the task to the queue if " 38 | "there's no executor available to execute it", 39 | () async { 40 | final task = CleanableFakeDelayingTask(Duration(milliseconds: 500)); 41 | 42 | Future onTaskRegistered(TaskRequest request) { 43 | return Future.value(SubmittedTaskEvent(task, null)); 44 | } 45 | 46 | final taskManager = TaskManager.private( 47 | {}, 48 | Queue(), 49 | StreamController(sync: true), 50 | onTaskRegistered, 51 | ); 52 | 53 | expect(taskManager.getPendingTasks(), isEmpty); 54 | 55 | taskManager.handle(TaskRequest(task, TaskTracker())); 56 | 57 | // we have to wait because the current implementation of 58 | // task manager use a stream to handle task request events 59 | // and keep them ordered. So to avoid having test failing because of 60 | // dart event loop not processing the stream event, even when we specified 61 | // sync to true. 62 | await Future.delayed(Duration(milliseconds: 500)); 63 | 64 | expect(taskManager.getPendingTasks(), hasLength(1)); 65 | 66 | expect(taskManager.getPendingTasks().first, equals(task)); 67 | }, 68 | ); 69 | 70 | test( 71 | "Should execute the task if there's a executor available to execute it", 72 | () async { 73 | final task = CleanableFakeDelayingTask(Duration(milliseconds: 500)); 74 | final mockExecutor = MockExecutor(0); 75 | 76 | Future onTaskRegistered(TaskRequest request) { 77 | return Future.value(SubmittedTaskEvent(task, mockExecutor)); 78 | } 79 | 80 | final taskManager = TaskManager.private( 81 | {}, 82 | Queue(), 83 | StreamController(sync: true), 84 | onTaskRegistered, 85 | ); 86 | 87 | expect(taskManager.getPendingTasks(), isEmpty); 88 | 89 | taskManager.handle(TaskRequest(task, TaskTracker())); 90 | 91 | // we have to wait because the current implementation of 92 | // task manager use a stream to handle task request events 93 | // and keep them ordered. So to avoid having test failing because of 94 | // dart event loop not processing the stream event, even when we specified 95 | // sync to true. 96 | await Future.delayed(Duration(milliseconds: 500)); 97 | 98 | expect(taskManager.getPendingTasks(), isEmpty); 99 | 100 | verify(mockExecutor.execute(task)).called(1); 101 | }, 102 | ); 103 | 104 | test( 105 | "Should dispose task manager if dispose is called", 106 | () async { 107 | final task = CleanableFakeDelayingTask(Duration(milliseconds: 500)); 108 | 109 | Future onTaskRegistered(TaskRequest request) { 110 | return Future.value(SubmittedTaskEvent(task, null)); 111 | } 112 | 113 | final taskHandler = StreamController(); 114 | 115 | final taskManager = TaskManager.private( 116 | {}, 117 | Queue(), 118 | // some would say that using the sync parameter 119 | // would make the streamController synchronous but nope... 120 | taskHandler, 121 | onTaskRegistered, 122 | ); 123 | 124 | expect(taskManager.getPendingTasks(), isEmpty); 125 | 126 | for (var i = 0; i < 10; i++) { 127 | taskManager.handle(TaskRequest(task, TaskTracker())); 128 | } 129 | 130 | // we have to wait because the current implementation of 131 | // task manager use a stream to handle task request events 132 | // and keep them ordered. So to avoid having test failing because of 133 | // dart event loop not processing the stream event, even when we specified 134 | // sync to true. 135 | await Future.delayed(Duration(milliseconds: 500)); 136 | 137 | expect(taskManager.getPendingTasks(), isNotEmpty); 138 | 139 | expect(taskManager.getInProgressTasks(), isNotEmpty); 140 | 141 | expect(taskManager.dispose(), completes); 142 | 143 | expect(taskHandler.isClosed, isTrue); 144 | 145 | expect(taskManager.getPendingTasks(), isEmpty); 146 | 147 | expect(taskManager.getInProgressTasks(), isEmpty); 148 | }, 149 | ); 150 | } 151 | -------------------------------------------------------------------------------- /lib/src/tasks/tasks.dart: -------------------------------------------------------------------------------- 1 | import "dart:async"; 2 | 3 | import "../../executorservices.dart"; 4 | 5 | /// A [Task] that does not have any argument. 6 | class ActionTask extends Task { 7 | /// Create a [Task] that run a [_function] without argument. 8 | ActionTask(this._function); 9 | 10 | final FutureOr Function() _function; 11 | 12 | @override 13 | FutureOr execute() => _function(); 14 | } 15 | 16 | /// A [Task] that require one parameter. 17 | class CallableTask extends Task { 18 | /// Create a [Task] that run a [_function] with the following [_argument]. 19 | CallableTask(this._argument, this._function); 20 | 21 | final P _argument; 22 | 23 | final FutureOr Function(P parameter) _function; 24 | 25 | @override 26 | FutureOr execute() => _function(_argument); 27 | } 28 | 29 | /// A [Task] that require two parameters. 30 | class Function2Task extends Task { 31 | /// Create a [Task] that run a [_function] 32 | /// with [_argument1] and [_argument2]. 33 | Function2Task(this._argument1, this._argument2, this._function); 34 | 35 | final P1 _argument1; 36 | 37 | final P2 _argument2; 38 | 39 | final FutureOr Function(P1 p1, P2 p2) _function; 40 | 41 | @override 42 | FutureOr execute() => _function(_argument1, _argument2); 43 | } 44 | 45 | /// A [Task] that require three parameters. 46 | class Function3Task extends Task { 47 | /// Create a [Task] that run a [_function] 48 | /// with [_argument1], [_argument2], [_argument3]. 49 | Function3Task( 50 | this._argument1, 51 | this._argument2, 52 | this._argument3, 53 | this._function, 54 | ); 55 | 56 | final P1 _argument1; 57 | 58 | final P2 _argument2; 59 | 60 | final P3 _argument3; 61 | 62 | final FutureOr Function(P1 p1, P2 p2, P3 p3) _function; 63 | 64 | @override 65 | FutureOr execute() => _function(_argument1, _argument2, _argument3); 66 | } 67 | 68 | /// A [Task] that require four parameters. 69 | class Function4Task extends Task { 70 | /// Create a [Task] that run a [_function] 71 | /// with [_argument1], [_argument2], [_argument3] and [_argument4]. 72 | Function4Task( 73 | this._argument1, 74 | this._argument2, 75 | this._argument3, 76 | this._argument4, 77 | this._function, 78 | ); 79 | 80 | final P1 _argument1; 81 | 82 | final P2 _argument2; 83 | 84 | final P3 _argument3; 85 | 86 | final P4 _argument4; 87 | 88 | final FutureOr Function(P1 p1, P2 p2, P3 p3, P4 p4) _function; 89 | 90 | @override 91 | FutureOr execute() { 92 | return _function(_argument1, _argument2, _argument3, _argument4); 93 | } 94 | } 95 | 96 | /// A [SubscribableTask] that does not have any argument. 97 | class SubscribableActionTask extends SubscribableTask { 98 | /// Create a [Task] that run a [_function] without argument. 99 | SubscribableActionTask(this._function); 100 | 101 | final Stream Function() _function; 102 | 103 | @override 104 | Stream execute() => _function(); 105 | } 106 | 107 | /// A [SubscribableTask] that require one parameter. 108 | class SubscribableCallableTask extends SubscribableTask { 109 | /// Create a [Task] that run a [_function] with the following [_argument]. 110 | SubscribableCallableTask(this._argument, this._function); 111 | 112 | final P _argument; 113 | 114 | final Stream Function(P parameter) _function; 115 | 116 | @override 117 | Stream execute() => _function(_argument); 118 | } 119 | 120 | /// A [SubscribableTask] that require two parameters. 121 | class SubscribableFunction2Task extends SubscribableTask { 122 | /// Create a [Task] that run a [_function] 123 | /// with [_argument1] and [_argument2]. 124 | SubscribableFunction2Task(this._argument1, this._argument2, this._function); 125 | 126 | final P1 _argument1; 127 | 128 | final P2 _argument2; 129 | 130 | final Stream Function(P1 p1, P2 p2) _function; 131 | 132 | @override 133 | Stream execute() => _function(_argument1, _argument2); 134 | } 135 | 136 | /// A [Task] that require three parameters. 137 | class SubscribableFunction3Task extends SubscribableTask { 138 | /// Create a [Task] that run a [_function] 139 | /// with [_argument1], [_argument2], [_argument3]. 140 | SubscribableFunction3Task( 141 | this._argument1, 142 | this._argument2, 143 | this._argument3, 144 | this._function, 145 | ); 146 | 147 | final P1 _argument1; 148 | 149 | final P2 _argument2; 150 | 151 | final P3 _argument3; 152 | 153 | final Stream Function(P1 p1, P2 p2, P3 p3) _function; 154 | 155 | @override 156 | Stream execute() => _function(_argument1, _argument2, _argument3); 157 | } 158 | 159 | /// A [Task] that require four parameters. 160 | class SubscribableFunction4Task extends SubscribableTask { 161 | /// Create a [Task] that run a [_function] 162 | /// with [_argument1], [_argument2], [_argument3] and [_argument4]. 163 | SubscribableFunction4Task( 164 | this._argument1, 165 | this._argument2, 166 | this._argument3, 167 | this._argument4, 168 | this._function, 169 | ); 170 | 171 | final P1 _argument1; 172 | 173 | final P2 _argument2; 174 | 175 | final P3 _argument3; 176 | 177 | final P4 _argument4; 178 | 179 | final Stream Function(P1 p1, P2 p2, P3 p3, P4 p4) _function; 180 | 181 | @override 182 | Stream execute() { 183 | return _function(_argument1, _argument2, _argument3, _argument4); 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /test/isolate_executor_web_tests.dart: -------------------------------------------------------------------------------- 1 | import "package:executorservices/src/services/executors/isolate_executor_web.dart" 2 | as web_executor; 3 | import "package:executorservices/src/tasks/task_event.dart"; 4 | import "package:executorservices/src/tasks/task_output.dart"; 5 | import "package:mockito/mockito.dart"; 6 | import "package:test/test.dart"; 7 | 8 | import "test_utils.dart"; 9 | 10 | void main() { 11 | group("isolate executor web state", () { 12 | test( 13 | "Should not be busy if is not executing a task", 14 | () { 15 | final mockOnTaskCompleted = MockOnTaskCompleted(); 16 | 17 | final executor = 18 | web_executor.IsolateExecutor("web", mockOnTaskCompleted); 19 | 20 | expect(executor.isBusy(), isFalse); 21 | }, 22 | ); 23 | 24 | test("Should be busy if is executing a task", () { 25 | final task = FakeDelayingTask(Duration(milliseconds: 500)); 26 | 27 | final mockOnTaskCompleted = MockOnTaskCompleted(); 28 | 29 | final executor = web_executor.IsolateExecutor("web", mockOnTaskCompleted); 30 | 31 | expect(executor.isBusy(), isFalse); 32 | 33 | executor.execute(task); 34 | 35 | expect(executor.isBusy(), isTrue); 36 | }); 37 | }); 38 | 39 | group( 40 | "isolate executor web on task completed", 41 | () { 42 | test( 43 | "Should return a success output if the task succeded without error", 44 | () async { 45 | final task = FakeDelayingTask(Duration(milliseconds: 500)); 46 | 47 | final mockOnTaskCompleted = MockOnTaskCompleted(); 48 | 49 | final executor = 50 | web_executor.IsolateExecutor("web", mockOnTaskCompleted); 51 | 52 | expect(executor.isBusy(), isFalse); 53 | 54 | executor.execute(task); 55 | 56 | await Future.delayed(Duration(seconds: 1)); 57 | 58 | verify( 59 | mockOnTaskCompleted.call( 60 | argThat(isA()), 61 | executor, 62 | ), 63 | ).called(1); 64 | 65 | expect(executor.isBusy(), isFalse); 66 | }, 67 | ); 68 | 69 | test( 70 | "Should return a failure output if the task failed with an error", 71 | () async { 72 | final task = FakeFailureTask(5); 73 | 74 | final mockOnTaskCompleted = MockOnTaskCompleted(); 75 | 76 | final executor = 77 | web_executor.IsolateExecutor("web", mockOnTaskCompleted); 78 | 79 | expect(executor.isBusy(), isFalse); 80 | 81 | executor.execute(task); 82 | 83 | await Future.delayed(Duration(seconds: 1)); 84 | 85 | verify( 86 | mockOnTaskCompleted.call( 87 | argThat(isA()), 88 | executor, 89 | ), 90 | ).called(1); 91 | 92 | expect(executor.isBusy(), isFalse); 93 | }, 94 | ); 95 | 96 | test( 97 | "Should emit subscribable task event if the subscribable task emit it", 98 | () async* { 99 | final task = FakeSubscribableTask(5); 100 | 101 | final mockOnTaskCompleted = MockOnTaskCompleted(); 102 | 103 | final executor = 104 | web_executor.IsolateExecutor("web", mockOnTaskCompleted); 105 | 106 | expect(executor.isBusy(), isFalse); 107 | 108 | executor.execute(task); 109 | 110 | await Future.delayed(Duration(seconds: 1)); 111 | 112 | verify( 113 | mockOnTaskCompleted.call( 114 | argThat(isA()), 115 | executor, 116 | ), 117 | ).called(5); 118 | }, 119 | ); 120 | 121 | test( 122 | "Should emit subscribable task error " 123 | "if the subscribable task throwed an error", 124 | () async { 125 | final task = FakeFailureSubscribableTask(5); 126 | 127 | final mockOnTaskCompleted = MockOnTaskCompleted(); 128 | 129 | final executor = 130 | web_executor.IsolateExecutor("web", mockOnTaskCompleted); 131 | 132 | expect(executor.isBusy(), isFalse); 133 | 134 | executor.execute(task); 135 | 136 | await Future.delayed(Duration(seconds: 1)); 137 | 138 | verify( 139 | mockOnTaskCompleted.call( 140 | argThat(isA()), 141 | executor, 142 | ), 143 | ).called(5); 144 | 145 | verify( 146 | mockOnTaskCompleted.call( 147 | argThat(isA()), 148 | executor, 149 | ), 150 | ).called(1); 151 | }, 152 | ); 153 | 154 | test( 155 | "Should emit subscribable task done if the task has completed", 156 | () async { 157 | final task = FakeSubscribableTask(1); 158 | 159 | final mockOnTaskCompleted = MockOnTaskCompleted(); 160 | 161 | final executor = 162 | web_executor.IsolateExecutor("web", mockOnTaskCompleted); 163 | 164 | expect(executor.isBusy(), isFalse); 165 | 166 | executor.execute(task); 167 | 168 | await Future.delayed(Duration(seconds: 1)); 169 | 170 | verify( 171 | mockOnTaskCompleted.call( 172 | argThat(isA()), 173 | executor, 174 | ), 175 | ).called(1); 176 | 177 | verify( 178 | mockOnTaskCompleted.call( 179 | argThat(isA()), 180 | executor, 181 | ), 182 | ).called(1); 183 | 184 | expect(executor.isBusy(), isFalse); 185 | }, 186 | ); 187 | 188 | test( 189 | "Should emit subscribable task cancelled if the task is cancelled", 190 | () async { 191 | final task = FakeSubscribableTask(100000); 192 | 193 | final mockOnTaskCompleted = MockOnTaskCompleted(); 194 | 195 | final executor = 196 | web_executor.IsolateExecutor("web", mockOnTaskCompleted); 197 | 198 | expect(executor.isBusy(), isFalse); 199 | 200 | executor.execute(task); 201 | 202 | await Future.delayed(Duration(seconds: 1)); 203 | 204 | verifyNever( 205 | mockOnTaskCompleted.call( 206 | argThat(isA()), 207 | executor, 208 | ), 209 | ); 210 | 211 | expect(executor.isBusy(), isTrue); 212 | 213 | executor.cancelSubscribableTask( 214 | CancelledSubscribableTaskEvent(task.identifier), 215 | ); 216 | 217 | await Future.delayed(Duration(seconds: 1)); 218 | 219 | verify( 220 | mockOnTaskCompleted.call( 221 | argThat(isA()), 222 | executor, 223 | ), 224 | ).called(1); 225 | 226 | expect(executor.isBusy(), isFalse); 227 | }, 228 | ); 229 | }, 230 | ); 231 | } 232 | -------------------------------------------------------------------------------- /lib/src/task_manager.dart: -------------------------------------------------------------------------------- 1 | import "dart:async"; 2 | import "dart:collection"; 3 | 4 | import "package:executorservices/executorservices.dart"; 5 | import "package:executorservices/src/tasks/task_event.dart"; 6 | import "package:executorservices/src/tasks/task_output.dart"; 7 | import "package:executorservices/src/tasks/task_tracker.dart"; 8 | import "package:meta/meta.dart" show visibleForTesting; 9 | 10 | typedef OnTaskRegistered = Future Function( 11 | TaskRequest request, 12 | ); 13 | 14 | /// [ExecutorService] tasks manager. 15 | class TaskManager { 16 | /// Create [TaskManager] with [onTaskRegistered]. 17 | /// 18 | /// [onTaskRegistered] allow you to specify 19 | /// on which [Executor] the [TaskManager] should run a specific task. 20 | factory TaskManager( 21 | OnTaskRegistered onTaskRegistered, 22 | ) { 23 | assert(onTaskRegistered != null, "onTaskRegistered cannot be null"); 24 | 25 | return TaskManager.private( 26 | {}, 27 | Queue(), 28 | StreamController(), 29 | onTaskRegistered, 30 | ); 31 | } 32 | 33 | /// Create [TaskManager] with [_inProgressTasks], [_pendingTasks], 34 | /// [_taskHandler] and [_onTaskRegistered]. 35 | @visibleForTesting 36 | TaskManager.private( 37 | this._inProgressTasks, 38 | this._pendingTasks, 39 | this._taskHandler, 40 | this._onTaskRegistered, 41 | ); 42 | 43 | /// A repertoire of currently running [Task] that are waiting to be completed. 44 | final Map _inProgressTasks; 45 | 46 | /// The pending [BaseTask] that need to be executed. 47 | final Queue _pendingTasks; 48 | 49 | /// A [StreamController] that handle all the new task submitted to the 50 | /// [ExecutorService]. 51 | final StreamController _taskHandler; 52 | 53 | final Future Function(TaskRequest taskRequest) 54 | _onTaskRegistered; 55 | 56 | /// Handle a [request] to be executed or queued for future run. 57 | void handle(TaskRequest request) { 58 | if (_inProgressTasks.containsKey(request.task.identifier)) { 59 | final newTask = request.task.clone(); 60 | 61 | if (newTask == null) { 62 | throw UnsupportedError( 63 | "There's already a submitted task with the same instance, " 64 | "override the clone method of your task's class if you want " 65 | "to submit the same instance of your task multiple times", 66 | ); 67 | } 68 | 69 | // update the request object with the new task. 70 | request = TaskRequest(newTask, request.taskCompleter); 71 | } 72 | 73 | // Save the task completer so we can manage the task update the task state. 74 | _inProgressTasks[request.task.identifier] = request.taskCompleter; 75 | 76 | if (!_taskHandler.hasListener) { 77 | _taskHandler.stream.asyncMap(_onTaskRegistered).listen(_handleTask); 78 | } 79 | 80 | _taskHandler.add(request); 81 | } 82 | 83 | /// Cancel a pending [SubscribableTask] before it's executed. 84 | /// This is useful when a [SubscribableTask] have been submitted but there's 85 | /// no available [Executor] to run it and the user cancel it before execution. 86 | void cancelTask(final SubscribableTask task) { 87 | final tracker = _inProgressTasks.remove(task.identifier); 88 | 89 | if (tracker is SubscribableTaskTracker) { 90 | tracker.complete(); 91 | } 92 | 93 | _pendingTasks.removeWhere( 94 | (pendingTask) => pendingTask.identifier == task.identifier, 95 | ); 96 | } 97 | 98 | /// Dispose the [TaskManager]. 99 | Future dispose() { 100 | _inProgressTasks.clear(); 101 | _pendingTasks.clear(); 102 | return _taskHandler.close(); 103 | } 104 | 105 | /// Callback that's called when a [Executor]'s done processing a [Task]. 106 | void onTaskOutput( 107 | final TaskOutput taskOutput, 108 | final Executor executor, 109 | ) { 110 | // Here if dart supported sealed class it's would be great. 111 | // todo: check for sealed class support for future dart versions. 112 | if (taskOutput is SuccessTaskOutput) { 113 | // this is a terminal event so we remove the task from the in progress 114 | final tracker = _inProgressTasks.remove(taskOutput.taskIdentifier); 115 | // notify the client of this tracker that the task's done. 116 | (tracker as TaskTracker).complete(taskOutput.result); 117 | } else if (taskOutput is FailedTaskOutput) { 118 | // this is a terminal event so we remove the task from the in progress. 119 | // notify the client that the task failed. 120 | _inProgressTasks 121 | .remove(taskOutput.taskIdentifier) 122 | .completeWithError(taskOutput.error); 123 | } else if (taskOutput is SubscribableTaskEvent) { 124 | final tracker = _inProgressTasks[taskOutput.taskIdentifier]; 125 | // notify the client of the task that a new event has been received. 126 | (tracker as SubscribableTaskTracker).addEvent(taskOutput.event); 127 | } else if (taskOutput is SubscribableTaskError) { 128 | final tracker = _inProgressTasks[taskOutput.taskIdentifier]; 129 | // notify the client of the task that a error has been received. 130 | (tracker as SubscribableTaskTracker).completeWithError(taskOutput.error); 131 | } else if (taskOutput is SubscribableTaskCancelled) { 132 | // this is a terminal event so we remove the task from the in progress. 133 | final tracker = _inProgressTasks.remove(taskOutput.taskIdentifier); 134 | // notify the client of this tracker that the task has terminated. 135 | (tracker as SubscribableTaskTracker).complete(); 136 | } else if (taskOutput is SubscribableTaskDone) { 137 | // this is a terminal event so we remove the task from the in progress. 138 | final tracker = _inProgressTasks.remove(taskOutput.taskIdentifier); 139 | // notify the client of this tracker that the task's done. 140 | (tracker as SubscribableTaskTracker).complete(); 141 | } 142 | 143 | if (_pendingTasks.isNotEmpty && !executor.isBusy()) { 144 | /// We give this task to the passed executor. 145 | _setupHookAndRunTask(_pendingTasks.removeFirst(), executor); 146 | } 147 | } 148 | 149 | /// Get the current running tasks. 150 | Map getInProgressTasks() => 151 | UnmodifiableMapView(_inProgressTasks); 152 | 153 | /// Get the current pending [Task]. 154 | List getPendingTasks() => UnmodifiableListView(_pendingTasks); 155 | 156 | void _handleTask(final SubmittedTaskEvent event) { 157 | if (event.executor == null) { 158 | _pendingTasks.addLast(event.task); 159 | } else { 160 | _setupHookAndRunTask(event.task, event.executor); 161 | } 162 | } 163 | 164 | void _setupHookAndRunTask( 165 | final BaseTask task, 166 | final Executor executor, 167 | ) { 168 | final tracker = _inProgressTasks[task.identifier]; 169 | 170 | if (tracker is SubscribableTaskTracker) { 171 | tracker 172 | ..setCancellationCallback( 173 | () => executor.cancelSubscribableTask( 174 | CancelledSubscribableTaskEvent(task.identifier), 175 | ), 176 | ) 177 | ..setPauseCallback( 178 | () => executor.pauseSubscribableTask( 179 | PauseSubscribableTaskEvent(task.identifier), 180 | ), 181 | ) 182 | ..setResumeCallback( 183 | () => executor.resumeSubscribableTask( 184 | ResumeSubscribableTaskEvent(task.identifier), 185 | ), 186 | ); 187 | } 188 | 189 | executor.execute(task); 190 | } 191 | } 192 | 193 | /// [TaskManager] request. 194 | class TaskRequest { 195 | /// Create a task manager request. 196 | const TaskRequest(this.task, this.taskCompleter); 197 | 198 | /// [BaseTask] to be managed by the [TaskManager]. 199 | final BaseTask task; 200 | 201 | /// [BaseTaskTracker] for completion. 202 | final BaseTaskTracker taskCompleter; 203 | } 204 | -------------------------------------------------------------------------------- /lib/src/services/executors/isolate_executor.dart: -------------------------------------------------------------------------------- 1 | import "dart:async"; 2 | import "dart:isolate"; 3 | 4 | import "package:executorservices/executorservices.dart"; 5 | import "package:executorservices/src/tasks/task_event.dart"; 6 | import "package:executorservices/src/tasks/task_output.dart"; 7 | import "package:executorservices/src/utils/utils.dart"; 8 | 9 | /// [Executor] that execute [Task] into a isolate. 10 | class IsolateExecutor extends Executor { 11 | /// Create an [IsolateExecutor] with the specified [identifier]. 12 | IsolateExecutor( 13 | this.identifier, 14 | OnTaskCompleted taskCompletion, 15 | ) : assert(taskCompletion != null, "taskCompletion can't be null"), 16 | super(taskCompletion); 17 | 18 | /// The identifier of the [IsolateExecutor]. 19 | final String identifier; 20 | 21 | /// The isolate that's will execute [Task] in a isolated environment. 22 | Isolate _isolate; 23 | 24 | /// The port to send command to the [_isolate]. 25 | SendPort _isolateCommandPort; 26 | 27 | /// The port that will receive the [_isolate]'s responses. 28 | ReceivePort _isolateOutputPort; 29 | 30 | /// The subscription to the [_isolateOutputPort] event stream. 31 | StreamSubscription _outputSubscription; 32 | 33 | var _processingTask = false; 34 | 35 | DateTime _lastUsage; 36 | 37 | @override 38 | void execute(final BaseTask task) async { 39 | _processingTask = true; 40 | 41 | if (_isolate == null) { 42 | await _initialize(); 43 | } 44 | 45 | _isolateCommandPort.send(task); 46 | 47 | _lastUsage = DateTime.now(); 48 | } 49 | 50 | @override 51 | void cancelSubscribableTask(CancelledSubscribableTaskEvent event) { 52 | _sendEventToIsolate(event); 53 | } 54 | 55 | @override 56 | void pauseSubscribableTask(PauseSubscribableTaskEvent event) { 57 | _sendEventToIsolate(event); 58 | } 59 | 60 | @override 61 | void resumeSubscribableTask(ResumeSubscribableTaskEvent event) { 62 | _sendEventToIsolate(event); 63 | // if a task have been resumed then we update the executor state. 64 | _processingTask = true; 65 | } 66 | 67 | @override 68 | bool isBusy() => _processingTask; 69 | 70 | @override 71 | Future kill() async { 72 | await _outputSubscription?.cancel(); 73 | _isolate?.kill(priority: Isolate.immediate); 74 | _isolateOutputPort?.close(); 75 | } 76 | 77 | @override 78 | DateTime lastUsage() => _lastUsage; 79 | 80 | Future _initialize() async { 81 | _isolateOutputPort = ReceivePort(); 82 | 83 | _isolate = await Isolate.spawn( 84 | IsolateExecutor._isolateSetup, 85 | _isolateOutputPort.sendPort, 86 | debugName: identifier, 87 | errorsAreFatal: false, 88 | ); 89 | 90 | // We create a multi-subscription output port. 91 | // This will allow us to get the first event and still listen 92 | // for subsequent events. 93 | final outputEvent = _isolateOutputPort.asBroadcastStream(); 94 | 95 | // We wait for the first output before listening to the other output event 96 | // The first output is the isolate command port. 97 | _isolateCommandPort = await outputEvent.first; 98 | 99 | // Un-definitely process the event in of the outputEvent. 100 | // Until the subscription is cancelled or the stream is closed. 101 | _outputSubscription = outputEvent.listen( 102 | (event) { 103 | if (event is TaskOutput) { 104 | // if the output is final or 105 | if (event is SuccessTaskOutput || 106 | event is FailedTaskOutput || 107 | event is SubscribableTaskCancelled || 108 | event is SubscribableTaskDone) { 109 | _processingTask = false; 110 | } else { 111 | _processingTask = true; 112 | } 113 | onTaskCompleted(event, this); 114 | _lastUsage = DateTime.now(); 115 | } 116 | }, 117 | ); 118 | } 119 | 120 | void _sendEventToIsolate(dynamic event) { 121 | assert( 122 | _isolate != null, 123 | "subscribable task pause requested but isolate is null. " 124 | "This is a bug please report", 125 | ); 126 | 127 | _isolateCommandPort.send(event); 128 | 129 | _lastUsage = DateTime.now(); 130 | } 131 | 132 | static void _isolateSetup(final SendPort executorOutputPort) async { 133 | // The isolate's command port 134 | final isolateCommandPort = ReceivePort(); 135 | 136 | // Send the isolate command port as first event. 137 | executorOutputPort.send(isolateCommandPort.sendPort); 138 | 139 | // the in progress subscribable tasks. 140 | // allow us to keep track of the subscription to every subscribe task 141 | // so we can cancel them. 142 | final inProgressSubscriptions = {}; 143 | 144 | // Iterate through all the event received in the isolate command port. 145 | await for (final message in isolateCommandPort) { 146 | try { 147 | if (message is Task) { 148 | final intermediary = message.execute(); 149 | // Notify that the task has succeeded. 150 | executorOutputPort.send( 151 | SuccessTaskOutput( 152 | message.identifier, 153 | intermediary is Future ? await intermediary : intermediary, 154 | ), 155 | ); 156 | } else if (message is SubscribableTask) { 157 | _handleSubscribableTask( 158 | message, 159 | executorOutputPort, 160 | inProgressSubscriptions, 161 | ); 162 | } else if (message is CancelledSubscribableTaskEvent) { 163 | await cleanupSubscription( 164 | message.taskIdentifier, 165 | inProgressSubscriptions, 166 | ); 167 | // Notify that the task has been successfully cancelled. 168 | executorOutputPort.send( 169 | SubscribableTaskCancelled(message.taskIdentifier), 170 | ); 171 | } else if (message is PauseSubscribableTaskEvent) { 172 | pauseSubscription( 173 | message.taskIdentifier, 174 | inProgressSubscriptions, 175 | ); 176 | } else if (message is ResumeSubscribableTaskEvent) { 177 | resumeSubscription( 178 | message.taskIdentifier, 179 | inProgressSubscriptions, 180 | ); 181 | } else { 182 | throw ArgumentError( 183 | "${message.runtimeType} messages are not supported," 184 | " your message should extends $Task or $SubscribableTask", 185 | ); 186 | } 187 | } on Object catch (error) { 188 | final taskError = TaskFailedException( 189 | error.runtimeType, 190 | error.toString(), 191 | ); 192 | // Notify that the task has failed paused. 193 | executorOutputPort.send( 194 | FailedTaskOutput(message.identifier, taskError), 195 | ); 196 | } 197 | } 198 | } 199 | 200 | static void _handleSubscribableTask( 201 | SubscribableTask message, 202 | SendPort executorOutputPort, 203 | Map inProgressSubscriptions, 204 | ) { 205 | final stream = message.execute(); 206 | 207 | // Ignoring the cancellation of the subscription 208 | // because it's the user who control when to cancel the stream. 209 | // We also cancel stream when it's stream is done. 210 | // ignore: cancel_subscriptions 211 | final subscription = stream.listen( 212 | (event) => executorOutputPort.send( 213 | SubscribableTaskEvent(message.identifier, event), 214 | ), 215 | onError: (error) => executorOutputPort.send( 216 | SubscribableTaskError( 217 | message.identifier, 218 | TaskFailedException(error.runtimeType, error.toString()), 219 | ), 220 | ), 221 | onDone: () async { 222 | // cleanup the subscription. 223 | await cleanupSubscription(message.identifier, inProgressSubscriptions); 224 | // notify that the subscribable task's done emitting data. 225 | executorOutputPort.send( 226 | SubscribableTaskDone(message.identifier), 227 | ); 228 | }, 229 | ); 230 | 231 | // keep the subscription so that it's can be cancelled, paused, resumed. 232 | inProgressSubscriptions[message.identifier] = subscription; 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /test/task_manager_task_outputs_test.dart: -------------------------------------------------------------------------------- 1 | import "dart:collection"; 2 | 3 | import "package:executorservices/src/exceptions.dart"; 4 | import "package:executorservices/src/task_manager.dart"; 5 | import "package:executorservices/src/tasks/task_output.dart"; 6 | import "package:executorservices/src/tasks/task_tracker.dart"; 7 | import "package:mockito/mockito.dart"; 8 | import "package:test/test.dart"; 9 | 10 | import "test_utils.dart"; 11 | 12 | void main() { 13 | test( 14 | "Should notify the task tracker client when task completed with success", 15 | () { 16 | final task = FakeDelayingTask(); 17 | final taskTracker = TaskTracker(); 18 | 19 | final taskManager = TaskManager.private( 20 | {task.identifier: taskTracker}, 21 | Queue(), 22 | null, 23 | null, 24 | ); 25 | 26 | expect(taskManager.getInProgressTasks(), hasLength(1)); 27 | 28 | final mockExecutor = MockExecutor(0); 29 | 30 | taskManager.onTaskOutput( 31 | SuccessTaskOutput(task.identifier, 100), 32 | mockExecutor, 33 | ); 34 | 35 | expect(taskTracker.progress(), completion(equals(100))); 36 | 37 | expect(taskManager.getInProgressTasks(), isEmpty); 38 | }, 39 | ); 40 | 41 | test( 42 | "Should notify the task tracker client when task completed with a failure", 43 | () { 44 | final task = FakeDelayingTask(); 45 | final taskTracker = TaskTracker(); 46 | 47 | final taskManager = TaskManager.private( 48 | {task.identifier: taskTracker}, 49 | Queue(), 50 | null, 51 | null, 52 | ); 53 | 54 | expect(taskManager.getInProgressTasks(), hasLength(1)); 55 | 56 | final mockExecutor = MockExecutor(0); 57 | 58 | taskManager.onTaskOutput( 59 | FailedTaskOutput( 60 | task.identifier, 61 | const TaskFailedException(NullThrownError, "fake null pointer errpr"), 62 | ), 63 | mockExecutor, 64 | ); 65 | 66 | expect( 67 | taskTracker.progress(), 68 | throwsA(const TypeMatcher()), 69 | ); 70 | 71 | expect(taskManager.getInProgressTasks(), isEmpty); 72 | }, 73 | ); 74 | 75 | test( 76 | "Should notify the subscribable task tracker client " 77 | "when a subscribable task emitted a event", 78 | () { 79 | final task = FakeSubscribableTask(10); 80 | final taskTracker = SubscribableTaskTracker(); 81 | 82 | final taskManager = TaskManager.private( 83 | {task.identifier: taskTracker}, 84 | Queue(), 85 | null, 86 | null, 87 | ); 88 | 89 | expect(taskManager.getInProgressTasks(), hasLength(1)); 90 | 91 | final mockExecutor = MockExecutor(0); 92 | 93 | final eventValue = task.max / 2; 94 | 95 | taskManager.onTaskOutput( 96 | SubscribableTaskEvent(task.identifier, eventValue), 97 | mockExecutor, 98 | ); 99 | 100 | expect( 101 | taskTracker.progress(), 102 | emits(equals(eventValue)), 103 | ); 104 | 105 | expect(taskManager.getInProgressTasks(), hasLength(1)); 106 | }, 107 | ); 108 | 109 | test( 110 | "Should notify the subscribable task tracker client " 111 | "when a subscribable task emitted a error", 112 | () { 113 | final task = FakeSubscribableTask(10); 114 | final taskTracker = SubscribableTaskTracker(); 115 | 116 | final taskManager = TaskManager.private( 117 | {task.identifier: taskTracker}, 118 | Queue(), 119 | null, 120 | null, 121 | ); 122 | 123 | expect(taskManager.getInProgressTasks(), hasLength(1)); 124 | 125 | final mockExecutor = MockExecutor(0); 126 | 127 | taskManager.onTaskOutput( 128 | SubscribableTaskError( 129 | task.identifier, 130 | const TaskFailedException( 131 | IntegerDivisionByZeroException, 132 | "fake zero division error", 133 | ), 134 | ), 135 | mockExecutor, 136 | ); 137 | 138 | expect( 139 | taskTracker.progress(), 140 | emitsError(const TypeMatcher()), 141 | ); 142 | 143 | expect(taskManager.getInProgressTasks(), hasLength(1)); 144 | }, 145 | ); 146 | 147 | test( 148 | "Should notify the subscribable task tracker client " 149 | "when a subscribable task has been cancelled", 150 | () { 151 | final task = FakeSubscribableTask(10); 152 | final taskTracker = SubscribableTaskTracker(); 153 | 154 | final taskManager = TaskManager.private( 155 | {task.identifier: taskTracker}, 156 | Queue(), 157 | null, 158 | null, 159 | ); 160 | 161 | expect(taskManager.getInProgressTasks(), hasLength(1)); 162 | 163 | final mockExecutor = MockExecutor(0); 164 | 165 | taskManager.onTaskOutput( 166 | SubscribableTaskCancelled(task.identifier), 167 | mockExecutor, 168 | ); 169 | 170 | expect( 171 | taskTracker.progress(), 172 | emitsInOrder([ 173 | neverEmits(const TypeMatcher()), 174 | neverEmits(isNull), 175 | emitsDone 176 | ]), 177 | ); 178 | 179 | expect(taskManager.getInProgressTasks(), isEmpty); 180 | }, 181 | ); 182 | 183 | test( 184 | "Should notify the subscribable task tracker client " 185 | "when a subscribable task is done emitting event", 186 | () { 187 | final task = FakeSubscribableTask(10); 188 | final taskTracker = SubscribableTaskTracker(); 189 | 190 | final taskManager = TaskManager.private( 191 | {task.identifier: taskTracker}, 192 | Queue(), 193 | null, 194 | null, 195 | ); 196 | 197 | expect(taskManager.getInProgressTasks(), hasLength(1)); 198 | 199 | final mockExecutor = MockExecutor(0); 200 | 201 | taskManager.onTaskOutput( 202 | SubscribableTaskDone(task.identifier), 203 | mockExecutor, 204 | ); 205 | 206 | expect( 207 | taskTracker.progress(), 208 | emitsInOrder([ 209 | neverEmits(const TypeMatcher()), 210 | neverEmits(isNull), 211 | emitsDone 212 | ]), 213 | ); 214 | 215 | expect(taskManager.getInProgressTasks(), isEmpty); 216 | }, 217 | ); 218 | 219 | test( 220 | "Should run the next pending task if there's" 221 | " one and the executor is not busy", 222 | () { 223 | final task = FakeSubscribableTask(10); 224 | final mockExecutor = MockExecutor(0); 225 | when(mockExecutor.isBusy()).thenReturn(false); 226 | 227 | final taskManager = TaskManager.private( 228 | {}, 229 | Queue()..add(task), 230 | null, 231 | null, 232 | ); 233 | 234 | expect(taskManager.getPendingTasks(), hasLength(1)); 235 | 236 | taskManager.onTaskOutput(null, mockExecutor); 237 | 238 | expect(taskManager.getPendingTasks(), isEmpty); 239 | 240 | verify(mockExecutor.execute(task)).called(1); 241 | }, 242 | ); 243 | 244 | group( 245 | "Should not run the next pending task", 246 | () { 247 | final task = FakeSubscribableTask(10); 248 | 249 | test( 250 | "if there's a pending task but the executor is busy", 251 | () { 252 | final mockExecutor = MockExecutor(0); 253 | 254 | final taskManager = 255 | TaskManager.private(null, Queue()..add(task), null, null); 256 | 257 | expect(taskManager.getPendingTasks(), hasLength(1)); 258 | 259 | when(mockExecutor.isBusy()).thenReturn(true); 260 | 261 | taskManager.onTaskOutput(null, mockExecutor); 262 | 263 | expect(taskManager.getPendingTasks(), hasLength(1)); 264 | 265 | verifyNever(mockExecutor.execute(task)); 266 | }, 267 | ); 268 | 269 | test( 270 | "if there's no pending tasks", 271 | () { 272 | final mockExecutor = MockExecutor(0); 273 | 274 | final taskManager = TaskManager.private(null, Queue(), null, null); 275 | 276 | expect(taskManager.getPendingTasks(), isEmpty); 277 | 278 | taskManager.onTaskOutput(null, mockExecutor); 279 | 280 | verifyZeroInteractions(mockExecutor); 281 | }, 282 | ); 283 | }, 284 | ); 285 | } 286 | -------------------------------------------------------------------------------- /test/executors_test.dart: -------------------------------------------------------------------------------- 1 | import "dart:async"; 2 | 3 | import "package:executorservices/executorservices.dart"; 4 | import "package:executorservices/src/task_manager.dart"; 5 | import "package:executorservices/src/tasks/task_event.dart"; 6 | import "package:executorservices/src/tasks/task_output.dart"; 7 | import "package:executorservices/src/tasks/task_tracker.dart"; 8 | import "package:executorservices/src/tasks/tasks.dart"; 9 | import "package:mockito/mockito.dart"; 10 | import "package:test/test.dart"; 11 | 12 | import "test_utils.dart"; 13 | 14 | void main() { 15 | test( 16 | "Should return the available executor if there's one that is not busy", 17 | () { 18 | final mockExecutor1 = MockExecutor(1); 19 | final mockExecutor2 = MockExecutor(2); 20 | 21 | when(mockExecutor1.isBusy()).thenAnswer((_) => false); 22 | when(mockExecutor2.isBusy()).thenAnswer((_) => true); 23 | 24 | final executorService = _FakeExecutorService( 25 | "fakeexecutor", 26 | 2, 27 | false, 28 | [mockExecutor1, mockExecutor2], 29 | ); 30 | 31 | final task = CleanableFakeDelayingTask(); 32 | final taskTracker = TaskTracker(); 33 | 34 | expect( 35 | executorService.createNewTaskEvent(TaskRequest(task, taskTracker)), 36 | completion(equals(SubmittedTaskEvent(task, mockExecutor1))), 37 | ); 38 | }, 39 | ); 40 | 41 | test( 42 | "Should return null if there's no available executor", 43 | () { 44 | final mockExecutor1 = MockExecutor(1); 45 | final mockExecutor2 = MockExecutor(2); 46 | 47 | when(mockExecutor1.isBusy()).thenAnswer((_) => true); 48 | when(mockExecutor2.isBusy()).thenAnswer((_) => true); 49 | 50 | final executorService = _FakeExecutorService( 51 | "fakeexecutor", 52 | 2, 53 | false, 54 | [mockExecutor1, mockExecutor2], 55 | ); 56 | 57 | final task = CleanableFakeDelayingTask(); 58 | final taskTracker = TaskTracker(); 59 | 60 | expect( 61 | executorService.createNewTaskEvent(TaskRequest(task, taskTracker)), 62 | completion(equals(SubmittedTaskEvent(task, null))), 63 | ); 64 | }, 65 | ); 66 | 67 | test( 68 | "should put the task into pending queue if there's no available executor", 69 | () { 70 | final mockExecutor1 = MockExecutor(1); 71 | 72 | when(mockExecutor1.isBusy()).thenAnswer((_) => true); 73 | 74 | final executorService = _FakeExecutorService( 75 | "fakeexecutor", 76 | 1, 77 | false, 78 | [mockExecutor1], 79 | ); 80 | 81 | expect(executorService.getTaskManager().getPendingTasks(), isEmpty); 82 | 83 | // we can't await for the result because the executor will never be 84 | // ready to process this task so we will wait forever. 85 | executorService.submitAction(() => print("should not be printe")); 86 | 87 | expect( 88 | // Workaround to wait for when the event-loop will process the 89 | // future task in the queue. 90 | // It's might make the test flaky but work as of now. 91 | Future.delayed( 92 | Duration(milliseconds: 500), 93 | executorService.getTaskManager().getPendingTasks, 94 | ), 95 | completion(hasLength(1)), 96 | ); 97 | }, 98 | ); 99 | 100 | test( 101 | "should execute the task if there's one available executor", 102 | () async { 103 | final mockExecutor1 = MockExecutor(1); 104 | 105 | when(mockExecutor1.isBusy()).thenAnswer((_) => false); 106 | 107 | final executorService = _FakeExecutorService( 108 | "fakeservice", 109 | 1, 110 | false, 111 | [mockExecutor1], 112 | ); 113 | 114 | final task = ActionTask(() => print("")); 115 | 116 | verifyNever(mockExecutor1.execute(task)); 117 | 118 | // we can't await for the result because the executor will never be 119 | // ready to process this task so we will wait forever. 120 | // ignore: unawaited_futures 121 | executorService.submit(task); 122 | 123 | await Future.delayed(Duration(milliseconds: 500)); 124 | 125 | verify(mockExecutor1.execute(task))..called(1); 126 | }, 127 | ); 128 | 129 | test( 130 | "should create a executer and execute the task if there's" 131 | " no free executor and the max concurrency allow it", 132 | () async { 133 | final mockExecutor1 = MockExecutor(1); 134 | final mockExecutor2 = MockExecutor(2); 135 | 136 | final executorService = _FakeExecutorService( 137 | "fakeservice", 138 | 2, 139 | false, 140 | [mockExecutor1], 141 | (configuration) => mockExecutor2, 142 | ); 143 | 144 | when(mockExecutor1.isBusy()).thenAnswer((_) => true); 145 | 146 | final task = ActionTask(() => print("")); 147 | 148 | // we can't await for the result because the executor will never be 149 | // ready to process this task so we will wait forever. 150 | // ignore: unawaited_futures 151 | executorService.submit(task); 152 | 153 | await Future.delayed(Duration(milliseconds: 500)); 154 | 155 | verify(mockExecutor2.execute(task))..called(1); 156 | }, 157 | ); 158 | 159 | test( 160 | "should remove unused executors if executors are more $maxNonBusyExecutors", 161 | () async { 162 | final mockExecutor1 = MockExecutor(1); 163 | final mockExecutor2 = MockExecutor(2); 164 | final mockExecutor3 = MockExecutor(3); 165 | final mockExecutor4 = MockExecutor(4); 166 | final mockExecutor5 = MockExecutor(5); 167 | final mockExecutor6 = MockExecutor(6); 168 | final mockExecutor7 = MockExecutor(7); 169 | final mockExecutor8 = MockExecutor(8); 170 | 171 | when(mockExecutor1.isBusy()).thenAnswer((_) => false); 172 | when(mockExecutor1.lastUsage()).thenReturn( 173 | DateTime.now().subtract(Duration(minutes: 1)), 174 | ); 175 | 176 | when(mockExecutor2.isBusy()).thenAnswer((_) => false); 177 | when(mockExecutor2.lastUsage()).thenReturn( 178 | DateTime.now().subtract(Duration(minutes: 2)), 179 | ); 180 | 181 | when(mockExecutor3.isBusy()).thenAnswer((_) => false); 182 | when(mockExecutor3.lastUsage()).thenReturn( 183 | DateTime.now().subtract(Duration(minutes: 3)), 184 | ); 185 | 186 | when(mockExecutor4.isBusy()).thenAnswer((_) => false); 187 | when(mockExecutor4.lastUsage()).thenReturn( 188 | DateTime.now().subtract(Duration(minutes: 4)), 189 | ); 190 | 191 | when(mockExecutor5.isBusy()).thenAnswer((_) => false); 192 | when(mockExecutor5.lastUsage()).thenReturn( 193 | DateTime.now().subtract(Duration(minutes: 5)), 194 | ); 195 | 196 | when(mockExecutor6.isBusy()).thenAnswer((_) => false); 197 | when(mockExecutor6.lastUsage()).thenReturn( 198 | DateTime.now().subtract(Duration(minutes: 6)), 199 | ); 200 | 201 | when(mockExecutor7.isBusy()).thenAnswer((_) => false); 202 | when(mockExecutor7.lastUsage()).thenReturn( 203 | DateTime.now().subtract(Duration(minutes: 7)), 204 | ); 205 | 206 | when(mockExecutor8.isBusy()).thenAnswer((_) => false); 207 | when(mockExecutor8.lastUsage()).thenReturn( 208 | DateTime.now().subtract(Duration(minutes: 8)), 209 | ); 210 | 211 | final executorService = _FakeExecutorService( 212 | "fakeexecutor", 213 | 8, 214 | true /* We are allowing the service to cleanup executors */, 215 | [ 216 | mockExecutor1, 217 | mockExecutor2, 218 | mockExecutor3, 219 | mockExecutor4, 220 | mockExecutor5, 221 | mockExecutor6, 222 | mockExecutor7, 223 | mockExecutor8, 224 | ], 225 | ); 226 | 227 | expect(executorService.getExecutors(), hasLength(8)); 228 | 229 | final task = CleanableFakeDelayingTask(); 230 | final taskTracker = TaskTracker(); 231 | 232 | await executorService.createNewTaskEvent(TaskRequest(task, taskTracker)); 233 | 234 | expect(executorService.getExecutors(), hasLength(7)); 235 | 236 | expect( 237 | executorService.getExecutors(), 238 | equals([ 239 | mockExecutor1, 240 | mockExecutor2, 241 | mockExecutor3, 242 | mockExecutor4, 243 | mockExecutor5, 244 | mockExecutor6, 245 | mockExecutor7, 246 | ]), 247 | ); 248 | }, 249 | ); 250 | 251 | test( 252 | "should return result even if there's a task with the same unique id", 253 | () { 254 | final executors = ExecutorService.newUnboundExecutor(); 255 | 256 | final task = CleanableFakeDelayingTask(); 257 | 258 | final results = >[]; 259 | 260 | for (var index = 0; index < 10; index++) { 261 | results.add(executors.submit(task)); 262 | } 263 | 264 | for (var index = 0; index < 10; index++) { 265 | results.add(executors.submit(FakeDelayingTask())); 266 | } 267 | 268 | expect(Future.wait(results), completes); 269 | }, 270 | ); 271 | } 272 | 273 | class _FakeExecutorService extends ExecutorService { 274 | _FakeExecutorService( 275 | String identifier, 276 | int maxConcurrency, 277 | // ignore: avoid_positional_boolean_parameters 278 | bool allowCleanup, 279 | List executors, [ 280 | this.onCreateExecutor, 281 | ]) : super( 282 | identifier, 283 | maxConcurrency, 284 | releaseUnusedExecutors: allowCleanup, 285 | availableExecutors: executors, 286 | ); 287 | 288 | final Executor Function(OnTaskCompleted callback) onCreateExecutor; 289 | 290 | @override 291 | Executor createExecutor(final OnTaskCompleted onTaskCompleted) => 292 | onCreateExecutor != null 293 | ? onCreateExecutor(onTaskCompleted) 294 | : MockExecutor(0); 295 | } 296 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Pub.dev](https://img.shields.io/pub/v/executorservices.svg?style=flat-square&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAeGVYSWZNTQAqAAAACAAFARIAAwAAAAEAAQAAARoABQAAAAEAAABKARsABQAAAAEAAABSASgAAwAAAAEAAgAAh2kABAAAAAEAAABaAAAAAAAAAEgAAAABAAAASAAAAAEAAqACAAQAAAABAAAAIKADAAQAAAABAAAAIAAAAAAQdIdCAAAACXBIWXMAAAsTAAALEwEAmpwYAAACZmlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNS40LjAiPgogICA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgogICAgICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgICAgICAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyIKICAgICAgICAgICAgeG1sbnM6ZXhpZj0iaHR0cDovL25zLmFkb2JlLmNvbS9leGlmLzEuMC8iPgogICAgICAgICA8dGlmZjpPcmllbnRhdGlvbj4xPC90aWZmOk9yaWVudGF0aW9uPgogICAgICAgICA8dGlmZjpSZXNvbHV0aW9uVW5pdD4yPC90aWZmOlJlc29sdXRpb25Vbml0PgogICAgICAgICA8ZXhpZjpDb2xvclNwYWNlPjE8L2V4aWY6Q29sb3JTcGFjZT4KICAgICAgICAgPGV4aWY6UGl4ZWxYRGltZW5zaW9uPjY0PC9leGlmOlBpeGVsWERpbWVuc2lvbj4KICAgICAgICAgPGV4aWY6UGl4ZWxZRGltZW5zaW9uPjY0PC9leGlmOlBpeGVsWURpbWVuc2lvbj4KICAgICAgPC9yZGY6RGVzY3JpcHRpb24+CiAgIDwvcmRmOlJERj4KPC94OnhtcG1ldGE+Ck0aSxoAAAaTSURBVFgJrVdbbBRVGP7OzOzsbmsXChIIiEQFRaIRhEKi0VRDjI++LIoPeHkhgRgeBCUCYY3iHTWGVHnxFhNpy6MXkMtCfLAENAGEAMGEgEBSLu1u2+3u7Mw5fv/MbrsFeiOeZHfOnMv/f//3X84ZYLytrc0e2HImOx8n9/yFv/d4OHtg08B4JmMN9P+3jjEK2axTkadwav8mnNxbxpmswbFdGv92GJzObgvnDRTGCEKNCaBYvWxZEK49/tsiOFYL6pJNyPUABgHVWTAmQOMEByWvBXOaV0dACFopM5KOkamqWi3K29I2Tu/LUHkHHKcJ3XmfgsVWcYkoctCV8xF3V+HM/pZQaaR8RCOHnzTGolAdCjqxbzFV0OrEwshqWqvUYCyEiyp/2viYMslBf+l9zHnyLTJjc23EXu26Sv/WDFSVm+0xnM++AxcdSNoL0dfjI8adrmWHzxjxy3v4rPTjBNab46C3Crldk0Ll24/Iqlu2mxmoKv/p93th+ndicnwBevp8aKOHtfpm0T7q3ThKzutY2vxpOJ0ho5vFZUNj4kYA8h4FTfsfHWj0luCHXBETVZwuAMQhN+4Ipd/4x0V+WWHGFI3ZDx5m/zMsn9YarhIgmYprOTDUBZls5Nf1f25AsW4JZhU8pB0nXFVP1Q38yXPUH6M/xYztyRl4pSWoS+1A+7WvIgBULiAqbaCDNFMt85SPrYceQUxvRpF+LKkY7rEcPG0H6CUzwoDwI8/RfkJV2bNw/YqHvm4fbnIlWju/C/UKAxUQVQAK7WkRydhhjjsxCRpGLi3x2LuPIJYSRKHinjG5gfuUUsh3CasW8td8JOpXoPXqt3xH6AaCiACE1DM43j2yHrHkYygVmOOVNBNltwPCkCqbunt7FEpFA8t2kL9OEMmX0Hb1myoIa4D6LYcfgjIZ9Oc5R+WqYq2svF0QJIABaKGnW9gQSQ56CCKefJlMfB0NtJH6cE61wHbiCLyoyJgaALKyFgTFYm9go46jMh7ljawa2oQFlgzkCGDyVElBWR2BaJj8ClqvBVLtDLYcXodY4gmUmO/DVTgRXQtirDEhXu7ttVDs1wg9LmilWBGUCZ6z8F7HPI68jSIPFpkYzhrOhm28IMRoHTAYuymZ/ar8CAyRaftLWE4SRku9FvGjt/GACN1AFvJdikCkmtbKJwylpkHLwTZkgkirUGvX1/THA0Kyoa9gob/AbJDEG5RNBswGOK7o58xgiaxRNXx3PCCMjtwwcBZEBlvY1LQT5dJquHUcCS8QUUFiToYBOrz6aGYsIKo1IUc3+L7I5V5hwWJNlhK8cXEL8/U1xOuZ/UQqtxsBIxeSsbSxgBDqi/0WCr0EIG6ImoV2ue3w0rCxaRtBrEEipeAmJBsCh2FjjQ1CFEKjVUwxKNdFzYNHcgRlGX0fMrHoCxjvVWh9CiZm+cxcTfqkmMttdFQsIzFRdUO+m+dLKWJBrhgREZX/wbNazfz+0DPTm4qtlwMvdV7Tb4xf8Z2AkU2Ss4OxXNlffcgE4xr/ML2qFVPmwg3UOmeeRj3Pa2PODTpDFsgxyRtwhlRdWLFk9+zUxJ8fnzJdPZtIeU2xRDCVd8SAu3xaI7KElSog2T7QbsVEVJCAVKNGvM7Q3VyueELd2HgDPlH5+Ogvl7fGguDFCY6bmOi4ehYV5wNPX/E9nAs81RUFKdWp8GpYvSKEhtaC4Nlh79O2dowxd051UNcQnRGlQl6W3bKleZtt5232+QtH19jJ+OdeLs/0IGQeKFRgPB2YfFA2nQRzNiirfsma0DsRmKqLbC4OXCbU6WKA4422un9uJ3FnEehfWJT2DgtAUNEVVoa0L7947A3lxj4kiDCHBYhstPhPqwWM7vbL5nJQUmcCXxmjGS8V70rwMa0XpBps51L9B4dXLtiCE6pX5EsbEQAdrTK0LARx+eg6Zcc+8vI9JjpVo1wSAfIu6jRDo2h83UVWLgYeOnkIPWC5epqbtFNuonfy3WbuNvXopeascQ4cPABsbuYpNVojXxnqEBAvXDy+1orZH9eCqG6XsJTLgbAiQgPS4DPgXcsyTn297Xvl3a0z5z+bZs1pXzb4oTI0C6rSap90eYYkphmYO2Y8/InxvLVuwx3yKVYBz4corbxK3ZAsYbNilm0Fmk7iYaS1/6sMXplyYIjRowOQXQTRnk5rAfHjXfO3+p73pgpPNbkt8lOMOvmTj1SJPQnWMCEY81opyy73FQqOxm4R1XzwoMwNtP8ArtQKBPNf6YAAAAAASUVORK5CYII=)](https://pub.dartlang.org/packages/executorservices) [![License](https://img.shields.io/github/license/bitsydarel/executorservices?style=flat-square&logo=github)](https://github.com/modulovalue/dart_fenwick_tree/blob/master/LICENSE) 2 | 3 | A library for Dart and Flutter developers. 4 | 5 | [license](https://github.com/bitsydarel/executorservices/blob/master/LICENSE). 6 | 7 | ## Description 8 | It allows you to execute code in isolates or any executor currently supported. 9 | 10 | Support concurrent execution of tasks or functions. 11 | 12 | Support cleanup of unused isolates. 13 | 14 | Support caching of isolate that allow you to reuse them (like thread). 15 | 16 | It's extendable. 17 | 18 | ## Usage 19 | 20 | ### For tasks that return either a value or an error: 21 | 22 | For submitting a top level or static function without arguments to the executorservice. 23 | For task that return a value or a future the executorService.submit* return a future, to track the progress of the task. 24 | ```dart 25 | import "dart:math"; 26 | import "package:executorservices/executorservices.dart"; 27 | 28 | void main() { 29 | final executorService = ExecutorService.newSingleExecutor(); 30 | 31 | final Future futureResult = executorService.submitAction(_randomInt); 32 | 33 | futureResult.then((result) => print(result)); 34 | } 35 | 36 | int _randomInt() { 37 | return Random.secure().nextInt(1000); 38 | } 39 | ``` 40 | 41 | For submitting a top level or static function with one argument to the executorservice. 42 | ```dart 43 | import "package:executorservices/executorservices.dart"; 44 | 45 | void main() { 46 | final executorService = ExecutorService.newSingleExecutor(); 47 | 48 | final Future futureResult = executorService.submitCallable(expensiveHelloWorld, "Darel Bitsy"); 49 | 50 | futureResult.then((result) => print(result)); 51 | } 52 | 53 | Future expensiveHelloWorld(final String name) async { 54 | await Future.delayed(Duration(seconds: 1)); 55 | return "Hellow world $name"; 56 | } 57 | ``` 58 | 59 | For submitting a top level or static function with many other arguments to the executorservice. 60 | The executorservice provide method like submitFunction2,submitFunction3,submitFunction4. 61 | ```dart 62 | import "package:executorservices/executorservices.dart"; 63 | 64 | void main() { 65 | final executorService = ExecutorService.newSingleExecutor(); 66 | 67 | final Future futureResult = executorService.submitFunction2( 68 | expensiveHelloWorld, 69 | "Darel Bitsy", 70 | 23, 71 | ); 72 | 73 | futureResult.then((result) => print(result)); 74 | } 75 | 76 | Future expensiveHelloWorld(final String name, final int age) async { 77 | await Future.delayed(Duration(seconds: 1)); 78 | if (age >= 80) { 79 | return "Hellow world elder $name"; 80 | } else { 81 | return "Hellow world $name"; 82 | } 83 | } 84 | ``` 85 | 86 | If you want to run a instance method of a existing class you need to make it extend the Task class. 87 | If if you want to have more than five arguments function. 88 | If you don't want to have the code as top level or static function. 89 | ```dart 90 | import "dart:async"; 91 | import "dart:isolate"; 92 | 93 | import "package:executorservices/executorservices.dart"; 94 | import "package:http/http.dart" as http; 95 | 96 | void main() { 97 | final executorService = ExecutorService.newSingleExecutor(); 98 | 99 | final Future futureResult = executorService.submit( 100 | GetPost("https://jsonplaceholder.typicode.com/posts", 5), 101 | ); 102 | 103 | futureResult.then( 104 | (result) => print( 105 | "Recieved $result and it's running from ${Isolate.current.debugName}", 106 | ), 107 | ); 108 | } 109 | 110 | class GetPost extends Task { 111 | GetPost(this.apiUrl, this.index); 112 | 113 | final String apiUrl; 114 | final int index; 115 | 116 | @override 117 | FutureOr execute() { 118 | return http 119 | .get("$apiUrl/$index") 120 | .then((postResponse) => postResponse.body) 121 | .then( 122 | (json) { 123 | print( 124 | "about to return $json and it's running from ${Isolate.current.debugName}", 125 | ); 126 | return json; 127 | }, 128 | ); 129 | } 130 | } 131 | ``` 132 | 133 | A example of chaining tasks. 134 | ```dart 135 | import "dart:async"; 136 | import "dart:math"; 137 | 138 | import "package:executorservices/executorservices.dart"; 139 | import "package:http/http.dart" as http; 140 | 141 | void main() { 142 | final executorService = ExecutorService.newSingleExecutor(); 143 | 144 | executorService 145 | .submitFunction2(_fullName, "Darel", "Bitsy") 146 | .then( 147 | (fullName) { 148 | print("Hi, $fullName"); 149 | return executorService.submitAction(randomPostId); 150 | }, 151 | ) 152 | .then( 153 | (postId) => executorService.submit( 154 | GetPost("https://jsonplaceholder.typicode.com/posts", postId), 155 | ), 156 | ) 157 | .then(print); 158 | } 159 | 160 | int randomPostId() { 161 | return Random.secure().nextInt(10); 162 | } 163 | 164 | Future _fullName(final String firstName, final String lastName) async { 165 | await Future.delayed(Duration(milliseconds: 500)); 166 | return "$firstName, $lastName"; 167 | } 168 | 169 | class GetPost extends Task { 170 | GetPost(this.apiUrl, this.index); 171 | 172 | final String apiUrl; 173 | final int index; 174 | 175 | @override 176 | FutureOr execute() { 177 | return http.get("$apiUrl/$index").then((postResponse) => postResponse.body); 178 | } 179 | } 180 | ``` 181 | 182 | ### For tasks that emit events (Code that return a stream): 183 | 184 | For executing to a top level or static function without arguments that return a stream to the executorservice. 185 | ```dart 186 | import "dart:async"; 187 | import "dart:isolate"; 188 | import "dart:math"; 189 | 190 | import "package:executorservices/executorservices.dart"; 191 | 192 | void main() { 193 | final executorService = ExecutorService.newSingleExecutor(); 194 | 195 | final Stream streamOfRandomNumber = 196 | executorService.subscribeToAction(randomGenerator); 197 | 198 | streamOfRandomNumber.listen( 199 | (newData) => print( 200 | "Received $newData and it's running on isolate: ${Isolate.current.debugName}", 201 | ), 202 | onError: (error) => print( 203 | "Received ${error.toString()} and it's running on isolate: ${Isolate.current.debugName}", 204 | ), 205 | onDone: () => print( 206 | "Task's done and it's running on isolate: ${Isolate.current.debugName}", 207 | ), 208 | ); 209 | } 210 | 211 | Stream randomGenerator() async* { 212 | for (var i = 0; i < Random.secure().nextInt(10); i++) { 213 | await Future.delayed(Duration(seconds: 1)); 214 | print( 215 | "about to yield $i and it's running from ${Isolate.current.debugName}", 216 | ); 217 | yield i; 218 | } 219 | } 220 | ``` 221 | 222 | Here are the analogical method for other type of top level, or static functions: 223 | subscribeToCallable, subscribeToFunction2, subscribeToFunction3, subscribeToFunction4 224 | 225 | If you want to run a instance method of a existing class you need to make it extend the SubscribableTask class. 226 | If if you want to have more than five arguments function. 227 | If you don't want to have the code as top level or static function. 228 | 229 | ```dart 230 | import "dart:async"; 231 | import "dart:isolate"; 232 | 233 | import "package:executorservices/executorservices.dart"; 234 | import "package:http/http.dart" as http; 235 | 236 | void main() { 237 | final executorService = ExecutorService.newSingleExecutor(); 238 | 239 | final Stream streamOfPosts = executorService.subscribe( 240 | GetPosts("https://jsonplaceholder.typicode.com/posts", 10), 241 | ); 242 | 243 | streamOfPosts.listen( 244 | (newData) => print( 245 | "Received $newData and it's running on isolate: ${Isolate.current.debugName}", 246 | ), 247 | onError: (error) => print( 248 | "Received ${error.toString()} and it's running on isolate: ${Isolate.current.debugName}", 249 | ), 250 | onDone: () => print( 251 | "Task's done and it's running on isolate: ${Isolate.current.debugName}", 252 | ), 253 | ); 254 | } 255 | 256 | class GetPosts extends SubscribableTask { 257 | GetPosts(this.apiUrl, this.maxPosts); 258 | 259 | final String apiUrl; 260 | final int maxPosts; 261 | 262 | @override 263 | Stream execute() async* { 264 | for (var index = 0; index < maxPosts; index++) { 265 | final postJson = await http 266 | .get("$apiUrl/$index") 267 | .then((postResponse) => postResponse.body); 268 | 269 | print( 270 | "about to yield $postJson and " 271 | "it's running from ${Isolate.current.debugName}", 272 | ); 273 | 274 | yield postJson; 275 | } 276 | } 277 | } 278 | ``` 279 | 280 | A example of chaining tasks of different type together. 281 | ```dart 282 | import "dart:async"; 283 | import "dart:io"; 284 | 285 | import "package:executorservices/executorservices.dart"; 286 | import "package:http/http.dart" as http; 287 | 288 | void main() { 289 | final executorService = ExecutorService.newUnboundExecutor(); 290 | 291 | executorService 292 | .subscribeToAction(randomPostIds) 293 | .asyncMap((randomPostId) => executorService.submit(GetPost(randomPostId))) 294 | .listen( 295 | (newData) => print("Received $newData"), 296 | onError: (error) => print("Received ${error.toString()}"), 297 | onDone: () { 298 | print("Task's done"); 299 | exit(0); 300 | }, 301 | ); 302 | } 303 | 304 | Stream randomPostIds() async* { 305 | for (var index = 0; index < 10; index++) { 306 | await Future.delayed(Duration(milliseconds: 100)); 307 | yield index; 308 | } 309 | } 310 | 311 | class GetPost extends Task { 312 | GetPost(this.postId); 313 | 314 | final int postId; 315 | 316 | @override 317 | Future execute() { 318 | return http 319 | .get("https://jsonplaceholder.typicode.com/posts/$postId") 320 | .then((postResponse) => postResponse.body); 321 | } 322 | } 323 | ``` 324 | 325 | By default you can't re-submit the same instance of a ongoing Task to a ExecutorService multiple times. 326 | Because the result of your submitted task is associated with the task instance identifier. 327 | So by default submitting the same instance of a task multiple times will result in unexpected behaviors. 328 | 329 | For example, this won't work: 330 | 331 | ```dart 332 | main() { 333 | final executors = ExecutorService.newUnboundExecutor(); 334 | 335 | final task = SameInstanceTask(); 336 | 337 | executors.submit(task); 338 | executors.submit(task); 339 | executors.submit(task); 340 | 341 | for (var index = 0; index < 10; index++) { 342 | executors.submit(task); 343 | } 344 | } 345 | 346 | class SameInstanceTask extends Task { 347 | @override 348 | FutureOr execute() async { 349 | await Future.delayed(Duration(seconds: 5)); 350 | return "Done executing same instance task"; 351 | } 352 | } 353 | ``` 354 | 355 | 356 | But if you want to submit the same instance of a task multiple times you need to override the Task is clone method. 357 | 358 | For example, this will now work: 359 | ```dart 360 | main() { 361 | final executors = ExecutorService.newUnboundExecutor(); 362 | 363 | final task = SameInstanceTask(); 364 | 365 | for (var index = 0; index < 10; index++) { 366 | executors.submit(task); 367 | } 368 | 369 | final taskWithParams = SameInstanceTaskWithParams("Darel Bitsy"); 370 | 371 | for (var index = 0; index < 10; index++) { 372 | executors.submit(taskWithParams); 373 | } 374 | } 375 | 376 | class SameInstanceTask extends Task { 377 | @override 378 | FutureOr execute() async { 379 | await Future.delayed(Duration(minutes: 5)); 380 | return "Done executing same instance task"; 381 | } 382 | 383 | @override 384 | SameInstanceTask clone() { 385 | return SameInstanceTask(); 386 | } 387 | } 388 | 389 | class SameInstanceTaskWithParams extends Task { 390 | SameInstanceTaskWithParams(this.name); 391 | 392 | final String name; 393 | 394 | @override 395 | FutureOr execute() async { 396 | await Future.delayed(Duration(minutes: 5)); 397 | return "Done executing same instance task with name: $name"; 398 | } 399 | 400 | @override 401 | SameInstanceTaskWithParams clone() { 402 | return SameInstanceTaskWithParams(name); 403 | } 404 | } 405 | ``` 406 | 407 | ## Features and bugs 408 | 409 | Please file feature requests and bugs at the [issue tracker][tracker]. 410 | 411 | [tracker]:https://github.com/bitsydarel/executorservices/issues 412 | -------------------------------------------------------------------------------- /lib/executorservices.dart: -------------------------------------------------------------------------------- 1 | /// Package that allow to execute dart code into a isolate. 2 | /// 3 | /// A variant of java executor api. 4 | library executorservices; 5 | 6 | import "dart:async"; 7 | 8 | import "dart:collection"; 9 | 10 | import "package:executorservices/src/exceptions.dart"; 11 | import "package:executorservices/src/services/isolate_executor_service.dart"; 12 | import "package:executorservices/src/task_manager.dart"; 13 | import "package:executorservices/src/tasks/task_event.dart"; 14 | import "package:executorservices/src/tasks/task_output.dart"; 15 | import "package:executorservices/src/tasks/task_tracker.dart"; 16 | import "package:executorservices/src/tasks/tasks.dart"; 17 | import "package:meta/meta.dart" show visibleForTesting, protected, factory; 18 | 19 | import "src/utils/utils.dart" 20 | if (dart.library.html) "src/utils/utils_web.dart" 21 | if (dart.library.io) "src/utils/io_utils.dart"; 22 | 23 | export "src/exceptions.dart"; 24 | 25 | /// Maximum allowed executors to be kept. 26 | const int maxNonBusyExecutors = 5; 27 | 28 | /// Class that execute [BaseTask]. 29 | abstract class Executor { 30 | /// Create a [Executor] with the [onTaskCompleted]. 31 | const Executor(this.onTaskCompleted); 32 | 33 | /// Callback that's called when a [Task] has completed. 34 | final OnTaskCompleted onTaskCompleted; 35 | 36 | /// Verify if the [Executor] is currently executing any [BaseTask]. 37 | bool isBusy(); 38 | 39 | /// Execute the [BaseTask] on the [Executor]. 40 | void execute(BaseTask task); 41 | 42 | /// Cancel any on going subscription to a [SubscribableTask]. 43 | void cancelSubscribableTask(CancelledSubscribableTaskEvent event); 44 | 45 | /// Pause any on going subscription to a [SubscribableTask]. 46 | void pauseSubscribableTask(PauseSubscribableTaskEvent event); 47 | 48 | /// Resume any on going subscription to a [SubscribableTask]. 49 | void resumeSubscribableTask(ResumeSubscribableTaskEvent event); 50 | 51 | /// Kill the [Executor], this is the best place to free all resources. 52 | FutureOr kill(); 53 | 54 | /// Get the last time this executor was used. 55 | DateTime lastUsage(); 56 | } 57 | 58 | /// A service that may execute [BaseTask] on many [Executor]. 59 | abstract class ExecutorService { 60 | /// Create a cached [ExecutorService], that's backed by many [Executor]. 61 | /// 62 | /// Note: It's unbound but restrict to the value of 2 ^ 63 so that you won't 63 | /// shoot yourself in the foot, if you want to go pass this 64 | /// use [ExecutorService.newFixedExecutor]. 65 | /// 66 | /// Note: use isolate if the platform support them. 67 | factory ExecutorService.newUnboundExecutor([ 68 | final String identifier = "io_isolate_service", 69 | ]) { 70 | return IsolateExecutorService(identifier, 2 ^ 63, allowCleanup: true); 71 | } 72 | 73 | /// Create a IO [ExecutorService], that's backed by a number [Executor] 74 | /// matching the number of cpu available. 75 | /// 76 | /// Note: use isolate if the platform support them. 77 | factory ExecutorService.newComputationExecutor([ 78 | final String identifier = "computation_isolate_service", 79 | ]) { 80 | return IsolateExecutorService(identifier, getCpuCount()); 81 | } 82 | 83 | /// Create a [ExecutorService] backed by a single [Executor]. 84 | /// 85 | /// Note: use isolate if the platform support them. 86 | factory ExecutorService.newSingleExecutor([ 87 | final String identifier = "single_isolate_service", 88 | ]) { 89 | return IsolateExecutorService(identifier, 1); 90 | } 91 | 92 | /// Create a [ExecutorService] backed by fixed number [Executor]. 93 | /// 94 | /// Note: use isolate if the platform support them. 95 | factory ExecutorService.newFixedExecutor( 96 | final int executorCount, [ 97 | final String identifier = "single_isolate_service", 98 | ]) { 99 | return IsolateExecutorService(identifier, executorCount); 100 | } 101 | 102 | /// Create a new [ExecutorService] instance. 103 | /// 104 | /// [identifier] of the [ExecutorService]. 105 | /// 106 | /// [maxExecutorCount] for how many executors can be used at time to 107 | /// execute a task, some [Executor] can run multiple tasks at a time. 108 | ExecutorService( 109 | this.identifier, 110 | this.maxExecutorCount, { 111 | this.releaseUnusedExecutors = false, 112 | final List availableExecutors, 113 | }) : assert(identifier != null, "identifier can't be null"), 114 | assert(maxExecutorCount > 0, "maxConcurrency should be at least 1"), 115 | assert( 116 | releaseUnusedExecutors 117 | ? maxExecutorCount > maxNonBusyExecutors 118 | : !releaseUnusedExecutors, // value is false, just inverse it. 119 | "releaseUnusedExecutors can only be true " 120 | "if the maxExecutorCount is greater than $maxNonBusyExecutors", 121 | ), 122 | assert( 123 | (availableExecutors != null && 124 | availableExecutors.length <= maxExecutorCount) || 125 | availableExecutors == null, 126 | "availableExecutors size can be at most equal to $maxExecutorCount", 127 | ), 128 | _executors = availableExecutors ?? []; 129 | 130 | /// The identifier of the [ExecutorService]. 131 | final String identifier; 132 | 133 | /// The maximum number of [Task] allowed to be run at a time. 134 | final int maxExecutorCount; 135 | 136 | /// Release the unused [Executor]. 137 | final bool releaseUnusedExecutors; 138 | 139 | /// The list of [Executor] that can run a [Task]. 140 | @protected 141 | final List _executors; 142 | 143 | TaskManager _taskManager; 144 | 145 | bool _shuttingDown = false; 146 | 147 | /// Submit a top level or static [function] without argument. 148 | /// 149 | /// Throws [TaskRejectedException] if the [ExecutorService] is shutting down. 150 | /// 151 | /// Throws a [TaskFailedException] if for some reason the [function] failed 152 | /// with a exception. 153 | /// 154 | /// Implement [Task] if the [function] is can't be a top level or static. 155 | Future submitAction(final FutureOr Function() function) { 156 | return submit(ActionTask(function)); 157 | } 158 | 159 | /// Submit a top level or static [function] with one argument. 160 | /// 161 | /// Throws [TaskRejectedException] if the [ExecutorService] is shutting down. 162 | /// Throws a [TaskFailedException] if for some reason the [function] failed 163 | /// with a exception. 164 | /// 165 | /// Implement [Task] if the [function] is can't be a top level or static. 166 | Future submitCallable( 167 | final FutureOr Function(P parameter) function, 168 | final P argument, 169 | ) { 170 | return submit(CallableTask(argument, function)); 171 | } 172 | 173 | /// Submit a top level or static [function] with his two arguments. 174 | /// 175 | /// Throws [TaskRejectedException] if the [ExecutorService] is shutting down. 176 | /// Throws a [TaskFailedException] if for some reason the [function] failed 177 | /// with a exception. 178 | /// 179 | /// Implement [Task] if the [function] is can't be a top level or static. 180 | Future submitFunction2( 181 | final FutureOr Function(P1 p1, P2 p2) function, 182 | final P1 argument1, 183 | final P2 argument2, 184 | ) { 185 | return submit(Function2Task(argument1, argument2, function)); 186 | } 187 | 188 | /// Submit a top level or static [function] with 3 arguments. 189 | /// 190 | /// Throws [TaskRejectedException] if the [ExecutorService] is shutting down. 191 | /// Throws a [TaskFailedException] if for some reason the [function] failed 192 | /// with a exception. 193 | /// 194 | /// Implement [Task] if the [function] is can't be a top level or static. 195 | Future submitFunction3( 196 | final FutureOr Function(P1 p1, P2 p2, P3 p3) function, 197 | final P1 argument1, 198 | final P2 argument2, 199 | final P3 argument3, 200 | ) { 201 | return submit(Function3Task(argument1, argument2, argument3, function)); 202 | } 203 | 204 | /// Submit a top level or static [function] with 4 arguments. 205 | /// 206 | /// Throws [TaskRejectedException] if the [ExecutorService] is shutting down. 207 | /// Throws a [TaskFailedException] if for some reason the [function] failed 208 | /// with a exception. 209 | /// 210 | /// Implement [Task] if the [function] is can't be a top level or static. 211 | Future submitFunction4( 212 | final FutureOr Function(P1 p1, P2 p2, P3 p3, P4 p4) function, 213 | final P1 argument1, 214 | final P2 argument2, 215 | final P3 argument3, 216 | final P4 argument4, 217 | ) { 218 | return submit( 219 | Function4Task(argument1, argument2, argument3, argument4, function), 220 | ); 221 | } 222 | 223 | /// Submit a [task] to the [ExecutorService]. 224 | /// 225 | /// Throws [TaskRejectedException] if the [ExecutorService] is shutting down. 226 | /// Throws [TaskFailedException] if for some reason the [task] failed 227 | /// with a exception. 228 | Future submit(Task task) { 229 | if (_shuttingDown) { 230 | return Future.error(TaskRejectedException(this)); 231 | } else { 232 | final taskTracker = TaskTracker(); 233 | 234 | getTaskManager().handle(TaskRequest(task, taskTracker)); 235 | 236 | return taskTracker.progress(); 237 | } 238 | } 239 | 240 | /// Subscribe to the events emitted by 241 | /// a top level or static [function] without argument. 242 | /// 243 | /// Throws [TaskRejectedException] if the [ExecutorService] is shutting down. 244 | /// 245 | /// Throws a [TaskFailedException] if for some reason the [function] failed 246 | /// with a exception. 247 | /// 248 | /// if the [function] is can't be a top level or static, 249 | /// you should Implement [SubscribableTask]. 250 | Stream subscribeToAction(final Stream Function() function) { 251 | return subscribe(SubscribableActionTask(function)); 252 | } 253 | 254 | /// Subscribe to the events emitted by 255 | /// a top level or static [function] with one argument. 256 | /// 257 | /// Throws [TaskRejectedException] if the [ExecutorService] is shutting down. 258 | /// Throws a [TaskFailedException] if for some reason the [function] failed 259 | /// with a exception. 260 | /// 261 | /// if the [function] is can't be a top level or static, 262 | /// you should Implement [SubscribableTask]. 263 | Stream subscribeToCallable( 264 | final Stream Function(P parameter) function, 265 | final P argument, 266 | ) { 267 | return subscribe(SubscribableCallableTask(argument, function)); 268 | } 269 | 270 | /// Subscribe to the events emitted by 271 | /// a top level or static [function] with his two arguments. 272 | /// 273 | /// Throws [TaskRejectedException] if the [ExecutorService] is shutting down. 274 | /// Throws a [TaskFailedException] if for some reason the [function] failed 275 | /// with a exception. 276 | /// 277 | /// if the [function] is can't be a top level or static, 278 | /// you should Implement [SubscribableTask]. 279 | Stream subscribeToFunction2( 280 | final Stream Function(P1 p1, P2 p2) function, 281 | final P1 argument1, 282 | final P2 argument2, 283 | ) { 284 | return subscribe(SubscribableFunction2Task(argument1, argument2, function)); 285 | } 286 | 287 | /// Subscribe to the events emitted by 288 | /// a top level or static [function] with 3 arguments. 289 | /// 290 | /// Throws [TaskRejectedException] if the [ExecutorService] is shutting down. 291 | /// Throws a [TaskFailedException] if for some reason the [function] failed 292 | /// with a exception. 293 | /// 294 | /// if the [function] is can't be a top level or static, 295 | /// you should Implement [SubscribableTask]. 296 | Stream subscribeToFunction3( 297 | final Stream Function(P1 p1, P2 p2, P3 p3) function, 298 | final P1 argument1, 299 | final P2 argument2, 300 | final P3 argument3, 301 | ) { 302 | return subscribe( 303 | SubscribableFunction3Task(argument1, argument2, argument3, function), 304 | ); 305 | } 306 | 307 | /// Subscribe to the events emitted by 308 | /// a top level or static [function] with 4 arguments. 309 | /// 310 | /// Throws [TaskRejectedException] if the [ExecutorService] is shutting down. 311 | /// Throws a [TaskFailedException] if for some reason the [function] failed 312 | /// with a exception. 313 | /// 314 | /// if the [function] is can't be a top level or static. 315 | /// Subscribe to the events emitted by 316 | Stream subscribeToFunction4( 317 | final Stream Function(P1 p1, P2 p2, P3 p3, P4 p4) function, 318 | final P1 argument1, 319 | final P2 argument2, 320 | final P3 argument3, 321 | final P4 argument4, 322 | ) { 323 | return subscribe( 324 | SubscribableFunction4Task( 325 | argument1, 326 | argument2, 327 | argument3, 328 | argument4, 329 | function, 330 | ), 331 | ); 332 | } 333 | 334 | /// Subscribe to the stream of events produced by [task]. 335 | Stream subscribe(SubscribableTask task) { 336 | if (_shuttingDown) { 337 | return Stream.error(TaskRejectedException(this)); 338 | } else { 339 | final taskTracker = SubscribableTaskTracker(); 340 | 341 | getTaskManager().handle(TaskRequest(task, taskTracker)); 342 | 343 | return taskTracker.progress(); 344 | } 345 | } 346 | 347 | /// Shutdown the [ExecutorService]. 348 | Future shutdown() async { 349 | _shuttingDown = true; 350 | 351 | for (final executor in _executors) { 352 | await executor.kill(); 353 | } 354 | 355 | await _taskManager.dispose(); 356 | } 357 | 358 | /// Create a new [Executor] to execute [Task]. 359 | Executor createExecutor(final OnTaskCompleted onTaskCompleted); 360 | 361 | /// Get the current list of [Executor]. 362 | List getExecutors() => UnmodifiableListView(_executors); 363 | 364 | /// Allow to verify if the [ExecutorService] 365 | /// can accept any [Task] or [Function]. 366 | FutureOr canSubmitTask() => !_shuttingDown; 367 | 368 | /// Create a [SubmittedTaskEvent] for [request] with 369 | /// the next available [Executor] to execute the [request]'s task. 370 | @visibleForTesting 371 | Future createNewTaskEvent( 372 | final TaskRequest request, 373 | ) async { 374 | final available = []; 375 | 376 | for (final executor in _executors) { 377 | if (!executor.isBusy()) { 378 | available.add(executor); 379 | } 380 | } 381 | 382 | available.sort( 383 | // sort the list in the way that the older is at the bottom 384 | (left, right) => right.lastUsage().compareTo(left.lastUsage()), 385 | ); 386 | 387 | if (releaseUnusedExecutors && available.length > maxNonBusyExecutors) { 388 | final releasableExecutor = available.last; 389 | _executors.remove(releasableExecutor); 390 | await releasableExecutor.kill(); 391 | } 392 | 393 | var executor = available.isNotEmpty ? available.first : null; 394 | 395 | // We create a new executor if we don't have any available executor 396 | // And if the provided maxConcurrency allows us. 397 | if (executor == null && _executors.length < maxExecutorCount) { 398 | executor = createExecutor(getTaskManager().onTaskOutput); 399 | _executors.add(executor); 400 | } 401 | 402 | if (request.taskCompleter is SubscribableTaskTracker) { 403 | (request.taskCompleter as SubscribableTaskTracker) 404 | .setCancellationCallback(() => _taskManager.cancelTask(request.task)); 405 | } 406 | 407 | return SubmittedTaskEvent(request.task, executor); 408 | } 409 | 410 | /// Get the current [ExecutorService]'s [TaskManager]. 411 | @visibleForTesting 412 | TaskManager getTaskManager() => 413 | _taskManager ??= TaskManager(createNewTaskEvent); 414 | } 415 | 416 | /// A class representing a unit of execution. 417 | abstract class BaseTask { 418 | /// Task identifier that allow us to identify a task through isolates. 419 | final Object identifier = createTaskIdentifier(); 420 | 421 | /// Execute the task. 422 | R execute(); 423 | 424 | /// Clone the task. 425 | /// 426 | /// This allow you to submit the same instance of your task multiple times. 427 | /// 428 | /// Note: You should always return a new instance of your task class. 429 | @factory 430 | BaseTask clone() => null; 431 | 432 | @override 433 | bool operator ==(Object other) => 434 | identical(this, other) || 435 | other is BaseTask && 436 | runtimeType == other.runtimeType && 437 | identifier == other.identifier; 438 | 439 | @override 440 | int get hashCode => identifier.hashCode; 441 | } 442 | 443 | /// A class representing a unit of execution that return a [Future] or [R] 444 | abstract class Task extends BaseTask> { 445 | /// Clone the [Task]. 446 | /// 447 | /// This allow you to submit the same instance of your task multiple times. 448 | /// 449 | /// Note: You should always return a new instance of your task class. 450 | @override 451 | @factory 452 | Task clone() => null; 453 | } 454 | 455 | /// A class representing a stream of unit of execution. 456 | abstract class SubscribableTask extends BaseTask> { 457 | /// Clone the [SubscribableTask]. 458 | /// 459 | /// This allow you to submit the same instance of your task multiple times. 460 | /// 461 | /// Note: You should always return a new instance of your task class. 462 | @override 463 | @factory 464 | SubscribableTask clone() => null; 465 | } 466 | --------------------------------------------------------------------------------