isolate_manager
Create long-lived isolates for single or multiple functions, with support for Web Workers (via an efficient generator) and WASM compilation.
Create long-lived isolates for single or multiple functions, with support for Web Workers (via an efficient generator) and WASM compilation.
^1.0.0^2.0.0^1.0.0^1.10.0^1.25.8^10.2.0English project snapshot. Visit GitHub for the latest content.
codecov Pub Version Pub Points Pub Downloads Pub Likes
PubStats Popularity PubStats Rank PubStats Dependents
Isolate Manager is a powerful Flutter/Dart package designed to simplify concurrent programming using isolates. It offers robust cross-platform support, including native, web (via JavaScript Workers), and WebAssembly (WASM).
Future/Stream if Web Workers are unavailable.ImType wrappers (ImNum, ImString, etc.) for reliable data transfer with web workers.build_runner.Add the following to your pubspec.yaml:
dependencies:
isolate_manager: ^latest_version # Use the latest version
dev_dependencies:
isolate_manager_generator: ^latest_version # Required for web worker generation
Then, run dart pub get or flutter pub get.
Functions intended to run in an isolate must be:
static method within a class, ORAdditionally, annotate these functions with @pragma('vm:entry-point') to prevent them from being removed by tree-shaking during compilation.
Note: Worker annotations like
@isolateManagerWorker,@isolateManagerSharedWorker, and@isolateManagerCustomWorkerare provided by theisolate_managerpackage. Make sure to import them if you use them in your code.
import 'package:isolate_manager/isolate_manager.dart';
@pragma('vm:entry-point')
int sum(List<int> numbers) {
return numbers.reduce((a, b) => a + b);
}
No additional setup is required!
To use isolates on the web, your Dart functions need to be compiled into JavaScript Workers.
Annotate Your Functions: Use specific annotations on the functions you want to be available as web workers. Run the generator after adding or modifying these.
@isolateManagerWorker: For one-off or single-function isolates.@isolateManagerSharedWorker: For shared, multi-function isolates.@isolateManagerCustomWorker: For custom isolate functions where you manage communication manually.Data Transfer Limitations:
dart:ui).num, String, bool, null), and Map or List collections containing these primitives, are directly transferable.ImType wrappers or serialize/deserialize your data manually.Generate JS Workers: Run the following command in your terminal:
dart run isolate_manager:generate
(See Web Worker Generator for more options.)
Type Handling: When using WASM, all int types (including those in collections) are treated as double. Isolate Manager provides a built-in converter to handle this automatically; you can disable it by setting enableWasmConverter: false if needed.
Transferables: On WASM, transferables (zero-copy data transfer) add unnecessary overhead and provide no performance benefit. By default, transferables are omitted when targeting WASM. You can re-enable them by setting enableWasmTransferables: true if needed.
Development Server Headers: If your app hangs when running with flutter run -d chrome --wasm, you might need to set specific headers. Try:
flutter run -d chrome --wasm --web-header=Cross-Origin-Opener-Policy=same-origin --web-header=Cross-Origin-Embedder-Policy=require-corp
The optional debugName parameter defaults to normal for create/run, custom for createCustom/runCustomFunction, and shared for createShared.
Ideal for fire-and-forget computations.
import 'package:isolate_manager/isolate_manager.dart';
@pragma('vm:entry-point')
@isolateManagerWorker // For web worker generation
int fibonacciRecursive(int n) {
if (n < 2) return n;
return fibonacciRecursive(n - 1) + fibonacciRecursive(n - 2);
}
void main() async {
// Option 1: Explicit worker parameters (useful if function name differs or for clarity)
final result1 = await IsolateManager.run(
() => fibonacciRecursive(40), // The actual function to execute
workerName: 'fibonacciRecursive', // Name used for JS worker mapping
workerParameter: 40, // Parameter for the JS worker
);
print('Fibonacci(40) - Option 1: $result1');
// Option 2: Automatic worker mapping (concise)
final result2 = await IsolateManager.runFunction(fibonacciRecursive, 40);
print('Fibonacci(40) - Option 2: $result2');
}
Reuse a single isolate for multiple different computations.
import 'package:isolate_manager/isolate_manager.dart';
@pragma('vm:entry-point')
@isolateManagerSharedWorker
Future<double> addNumbersFuture(List<double> values) async {
return values[0] + values[1];
}
@pragma('vm:entry-point')
@isolateManagerSharedWorker
int multiplyNumbers(List<int> values) {
return values[0] * values[1];
}
void main() async {
final sharedIsolate = IsolateManager.createShared(
concurrent: 2, // Number of tasks this isolate can handle concurrently
useWorker: true, // Enable web worker usage if on web
workerMappings: {
// Map Dart function references to their JS worker names
addNumbersFuture: 'addNumbersFuture',
multiplyNumbers: 'multiplyNumbers',
},
);
// Optional: Listen for intermediate values (if any function sends them)
sharedIsolate.stream.listen((value) {
print('Intermediate value from shared isolate: $value');
});
final sumResult = await sharedIsolate.compute(addNumbersFuture, [10.5, 20.3]);
print('Sum (shared): 10.5 + 20.3 = $sumResult');
final productResult = await sharedIsolate.compute(multiplyNumbers, [7, 6]);
print('Product (shared): 7 * 6 = $productResult');
await sharedIsolate.stop(); // Important to release resources
// Or use `sharedIsolate.restart()` to restart the isolate
}
Dedicate an isolate to a single, potentially continuous, function.
import 'package:isolate_manager/isolate_manager.dart';
@pragma('vm:entry-point')
@isolateManagerWorker
int complexCalculation(int initialValue) {
// Simulate a task that might send progress updates or run for a while
int result = initialValue;
for (int i = 0; i < 5; i++) {
result += i * 2;
// If this were a custom function, you could send progress here
}
return result;
}
void main() async {
final singleFuncIsolate = IsolateManager.create(
complexCalculation, // The function this isolate is dedicated to
workerName: 'complexCalculation', // For JS worker
concurrent: 1, // Typically 1 for a single dedicated function
);
// Optional: Listen for intermediate values
singleFuncIsolate.stream.listen((value) {
print('Intermediate value from single-function isolate: $value');
});
final calculationResult = await singleFuncIsolate.compute(10); // `compute` is callable
print('Complex Calculation Result: $calculationResult');
await singleFuncIsolate.stop(); // Release resources
// Or use `singleFuncIsolate.restart()` to restart the isolate
}
For fine-grained control over the isolate's behavior, including sending multiple results or handling initialization/disposal.
import 'dart:convert';
import 'package:isolate_manager/isolate_manager.dart';
@pragma('vm:entry-point')
@isolateManagerWorker // For web
int fibonacci(int n) {
if (n < 0) throw ArgumentError('Input cannot be negative.');
if (n < 2) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
@pragma('vm:entry-point')
@isolateManagerCustomWorker
void customFibonacciWorker(dynamic params) {
IsolateManagerFunction.customFunction<int, int>(
params,
onEvent: (controller, message) {
try {
final result = fibonacci(message);
controller.sendResult(result); // Send the final result
} catch (err, stack) {
// Send an IsolateException for structured error handling
controller.sendResultError(IsolateException(err.toString(), stack.toString()));
}
return 0; // Indicates completion for this event
},
onInit: (controller) {
print('Custom Fibonacci Worker: Initialized');
// Perform any setup logic here
},
onDispose: (controller) {
print('Custom Fibonacci Worker: Disposed');
// Perform any cleanup logic here
},
autoHandleException: false, // Set to true to let IsolateManager handle basic errors
autoHandleResult: false, // Set to true to let IsolateManager handle basic result sending
);
}
void main() async {
final customIsolate = IsolateManager.createCustom(
customFibonacciWorker,
workerName: 'customFibonacciWorker', // For JS Worker
debugMode: true, // Enable more logging
);
try {
final result = await customIsolate.compute(10);
print('Custom Fibonacci(10): $result');
final resultNegative = await customIsolate.compute(-5); // This will throw
print('Custom Fibonacci(-5): $resultNegative');
} on IsolateException catch (e) {
print('Caught IsolateException: ${e.error}');
// print('Stack trace: ${e.stacktrace}');
} catch (e) {
print('Caught other error: $e');
} finally {
await customIsolate.stop();
}
}
Control how tasks are queued and processed when multiple requests are made to an isolate.
Priority Tasks: Add priority: true when calling compute or run to move a task to the front of its queue.
Queue Size Limit: Set maxCount in IsolateManager.create, createShared, or createCustom constructors to limit the number of pending tasks.
Queue Strategies: Define behavior when maxCount is reached:
UnlimitedStrategy(): (Default) No limit on queued tasks.DropNewestStrategy(): Removes the most recently added task.DropOldestStrategy(): Removes the oldest task in the queue.RejectIncomingStrategy(): Rejects any new tasks if the queue is full.Custom Queue Strategy: For custom logic, extend QueueStrategy<R, P>:
class MyCustomStrategy<R, P> extends QueueStrategy<R, P> {
@override
bool continueIfMaxCountExceeded() {
// Access `queues`, `queuesCount`, `maxCount`
if (queuesCount >= maxCount && queues.isNotEmpty) {
// Example: Allow if the oldest task is low priority (pseudo-code)
// if (queues.first.priority == false) {
// print('Custom strategy: Dropping oldest to make space.');
// queues.removeFirst(); // You'd need to manage this carefully
// return true; // Allow the new task
// }
return false; // Reject by default if full
}
return true; // Allow if not full
}
}
Note: Modifying queues directly in continueIfMaxCountExceeded requires careful implementation to ensure consistency.
Receive intermediate results from a task before it completes. This is typically used with IsolateManager.createCustom.
In this example, progress updates are sent as JSON strings for demonstration purposes.
// In your main Dart code:
void main() async {
final isolate = IsolateManager.createCustom(
longRunningTaskWithProgress,
workerName: 'longRunningTaskWithProgress', // For JS Worker
);
print('Starting task with progress updates...');
final result = await isolate.compute(
100, // Example parameter for the task
callback: (dynamic value) {
// `value` is what `controller.sendResult()` sends from the isolate
try {
final data = jsonDecode(value as String); // Assuming JSON string for progress
if (data.containsKey('progress')) {
print('Progress: ${data['progress']}%');
return false; // Indicates this is a progress update, not the final result
} else if (data.containsKey('finalResult')) {
print('Final result package received: ${data['finalResult']}');
return true; // Indicates this is the final result
}
} catch (e) {
print('Error decoding progress/result: $e');
return true; // Treat as error, stop listening
}
return true; // Default to stop if format is unexpected
},
);
print('Task completed with final processed value: $result');
await isolate.stop();
}
// In your isolate function (e.g., custom worker):
@pragma('vm:entry-point')
@isolateManagerCustomWorker
void longRunningTaskWithProgress(dynamic params) {
IsolateManagerFunction.customFunction<String, int>(
params,
onEvent: (controller, totalSteps) {
for (int i = 0; i <= totalSteps; i += 10) {
final progressReport = jsonEncode({'progress': i});
controller.sendResult(progressReport); // Send progress update
}
// Send the final result
return jsonEncode({'finalResult': totalSteps});
},
);
}
When working with Web Workers, data transfer is restricted. IsolateManager provides ImType wrappers for common types to ensure they are correctly (de)serialized.
import 'package:isolate_manager/isolate_manager.dart';
@pragma('vm:entry-point')
@isolateManagerWorker
ImMap processDataWeb(ImList numbers) {
// 1. Unwrap to get standard Dart types (List<dynamic> in this case)
final nativeList = numbers.unwrap as List<dynamic>;
// 2. Process the data
final processedMap = <ImType, ImType>{};
for (var i = 0; i < nativeList.length; i++) {
final numVal = nativeList[i] as num; // Ensure type
processedMap[ImString('item_$i')] = ImNum(numVal * 2);
}
// 3. Wrap the result in an ImType
return ImMap(processedMap);
}
void main() async {
// On web, this will use the JS worker if generated.
final isolate = IsolateManager.create(
processDataWeb,
workerName: 'processDataWeb',
);
try {
// 1. Wrap your input data
final inputData = ImList.wrap([1, 2.5, 3]);
// 2. Compute
final ImMap result = await isolate.compute(inputData) as ImMap;
// 3. Unwrap the result
final nativeResult = result.unwrap as Map<dynamic, dynamic>;
nativeResult.forEach((key, value) {
print('Web Processed: ${key} -> ${value}');
});
} on UnsupportedImTypeException catch (e) {
print('Error: Unsupported type used with ImType. ${e.message}');
} catch (e) {
print('An error occurred: $e');
} finally {
await isolate.stop();
}
}
Available ImType wrappers (for non-nullable types only):
ImNum(double value) / ImNum(int value)ImString(String value)ImBool(bool value)ImList(List<ImType> value) or ImList.wrap(List<dynamic> nativeList)ImMap(Map<ImType, ImType> value) or ImMap.wrap(Map<dynamic, dynamic> nativeMap)An UnsupportedImTypeException is thrown if ImList.wrap or ImMap.wrap encounters a type that cannot be converted.
For large Uint8List or ByteBuffer payloads, pass a transferables list to compute() to enable zero-copy transport instead of copying bytes across isolate boundaries.
@pragma('vm:entry-point')
@isolateManagerWorker
Uint8List processImage(Uint8List data) {
// ... image processing ...
return data;
}
final manager = IsolateManager.create(processImage, workerName: 'processImage');
await manager.start();
final pixels = Uint8List(1920 * 1080 * 4); // ~8 MB RGBA frame
final result = await manager.compute(
pixels,
transferables: [pixels.buffer], // zero-copy send
);
On native (VM) the buffer is wrapped in a TransferableTypedData and sent O(1) — no byte copying. On web (dart2js) the ArrayBuffer is transferred via the postMessage transfer list, also O(1), and the source buffer is detached (its lengthInBytes becomes 0) after the call returns.
sendResultWithAutoTransferInside a custom isolate, the AutoTransferExtension recursively finds every Uint8List / ByteBuffer in the result and transfers them automatically — no manual bookkeeping needed:
import 'package:isolate_manager/isolate_manager.dart';
@pragma('vm:entry-point')
void processingWorker(dynamic params) {
final controller =
IsolateManagerController<Map<String, Object?>, Uint8List>(params);
controller.onIsolateMessage.listen((input) {
final output = Uint8List(input.length);
for (var i = 0; i < output.length; i++) {
output[i] = (input[i] + 1) % 256;
}
// Finds all Uint8List/ByteBuffer in the map and transfers them zero-copy.
controller.sendResultWithAutoTransfer({'result': output, 'size': output.length});
});
controller.initialized();
}
| Native (VM) | Web — dart2js | Web — dart2wasm | |
|---|---|---|---|
| No transferables | Bytes deep-copied O(n) | Bytes serialised & copied O(n) | Bytes copied O(n) |
transferables: [data.buffer] |
Zero-copy via TransferableTypedData (O(1) transport; small codec overhead) |
Zero-copy via ArrayBuffer transfer (O(1)); source buffer detached |
⚠ No benefit — WASM linear memory must be copied to the JS heap regardless |
Pre-built TransferableTypedData |
Fastest — skips codec overhead entirely | N/A | N/A |
Native (VM):
TransferableTypedData before calling compute() removes the codec overhead and is the fastest option.TransferableTypedData; do not reuse the original Uint8List after calling compute() with it as a transferable.Web — dart2js:
ArrayBuffer is transferred in O(1); the worker receives the original memory.compute() returns — data.buffer.lengthInBytes becomes 0. Keep a reference to the result instead.Web — dart2wasm:
ArrayBuffer, so using transferables adds codec overhead with no speed benefit.transferables when targeting WASM.enableWasmTransferables: true in the IsolateManager constructor.To ensure custom exceptions are correctly propagated from Web Workers:
IsolateException.IsolateManager.registerException() in your main application.import 'package:isolate_manager/isolate_manager.dart';
// 1. Define your custom exception
class MyCustomWebException extends IsolateException {
const MyCustomWebException(super.error, [super.stacktrace]);
@override
String get name => 'MyCustomWebException'; // Crucial for deserialization
}
@pragma('vm:entry-point')
@isolateManagerWorker
ImNum taskThatThrowsCustom(ImNum input) {
if (input.unwrap == 0) {
throw const MyCustomWebException('Input cannot be zero!');
}
return ImNum(100 / (input.unwrap as num));
}
void main() async {
// 2. Register the exception type (typically in your app's initialization)
IsolateManager.registerException(
(message, stackTrace) => MyCustomWebException(message, stackTrace),
);
final isolate = IsolateManager.create(
taskThatThrowsCustom,
workerName: 'taskThatThrowsCustom', // For JS Worker
);
try {
print('Trying with valid input...');
final result = await isolate.compute(ImNum(10));
print('Result: ${(result as ImNum).unwrap}'); // Should be 10
print('\nTrying with input that causes custom exception...');
await isolate.compute(ImNum(0)); // This will throw
} on MyCustomWebException catch (e, s) {
print('Caught MyCustomWebException!');
print('Error: ${e.error}');
// print('Stack: $s'); // s is the stacktrace from the main isolate
// print('Original Isolate Stack: ${e.stacktrace}'); // stacktrace from the worker
} catch (e) {
print('Caught an unexpected error: $e');
} finally {
await isolate.stop();
}
}
Typically done during app initialization:
IsolateManager.addWorkerMapping(
fibonacciRecursive, // Dart function
'fibonacciRecursive', // Generated JS-worker file name
);
IsolateManager.addWorkerMapping(addNumbersFuture, 'addNumbersFuture');
Use the mapped function:
// No boilerplate — the manager already knows which worker to spin up.
await IsolateManager.runFunction(fibonacciRecursive, 40);
await sharedIsolate.compute(addNumbersFuture, [10.5, 20.3]);
await singleFuncIsolate.compute(10);
If you’re wondering what “magic” was removed:
// Long form (now unnecessary):
// final result = await IsolateManager.run(
// () => fibonacciRecursive(40),
// workerName: 'fibonacciRecursive',
// workerParameter: 40,
// );
Use the isolate_manager:generate command to compile annotated Dart functions into JavaScript workers for web deployment.
See Web (JavaScript Workers) for web platform setup.
Command:
dart run isolate_manager:generate
Flags & Options:
--single: Generate only for functions annotated with @isolateManagerWorker.--shared: Generate only for functions annotated with @isolateManagerSharedWorker.--in <path> (or -i <path>): Specify the input directory to scan for annotated functions (default: lib).--out <path> (or -o <path>): Specify the output directory for generated JS files (default: web).--obfuscate <level>: Set JavaScript obfuscation level (0-4, default is 4 for smallest size). 0 means no obfuscation.--debug: Retain temporary files created during generation for debugging purposes.--worker-mappings-experiment=lib/main.dart (Experimental): Attempt to auto-generate workerMappings for IsolateManager.createShared by scanning the specified Dart file.You can also pass additional arguments to the underlying Dart compiler by adding -- after the generator command. For example:
dart run isolate_manager:generate -- --omit-implicit-checks --no-source-maps
These arguments will be forwarded to the Dart process (useful for dart2js / dart compile js or other Dart compiler options).
isolateManagerInstance.queuesLength to get the current number of tasks in the queue.isolateManagerInstance.ensureStarted() to await explicit initialization if needed. Check isolateManagerInstance.isStarted to see if the isolate is ready.The following benchmarks demonstrate the performance of recursive Fibonacci calculations across different concurrency approaches and environments. Measurements are in microseconds (µs) and represent the median of 30 samples to ensure stability. Benchmarked on a MacBook M1 Pro 14" with 16GB RAM.
| Fibonacci | Main App | One Isolate | Three Isolates | IsolateManager.runFunction | IsolateManager.run | Isolate.run |
|---|---|---|---|---|---|---|
| 26 | 1,577 | 1,576 | 572 | 1,731 | 1,661 | 1,615 |
| 28 | 4,140 | 4,166 | 1,477 | 4,188 | 4,178 | 4,132 |
| 30 | 10,892 | 10,881 | 4,530 | 11,207 | 10,793 | 10,765 |
| Fibonacci | Main App | One Isolate | Three Isolates | IsolateManager.runFunction | IsolateManager.run | Isolate.run (Unsupported) |
|---|---|---|---|---|---|---|
| 26 | 5,108 | 1,333 | 596 | 8,607 | 8,797 | 0 |
| 28 | 13,486 | 3,256 | 1,340 | 10,156 | 10,683 | 0 |
| 30 | 34,990 | 8,500 | 4,000 | 15,230 | 15,000 | 0 |
| Fibonacci | Main App | One Isolate | Three Isolates | IsolateManager.runFunction | IsolateManager.run | Isolate.run (Unsupported) |
|---|---|---|---|---|---|---|
| 26 | 504 | 1,320 | 594 | 8,424 | 8,422 | 0 |
| 28 | 1,303 | 3,220 | 1,323 | 10,660 | 10,266 | 0 |
| 30 | 3,379 | 8,370 | 3,960 | 14,800 | 15,010 | 0 |
For more details, see the full benchmark information.
Contributions, issues, and feature requests are welcome! Feel free to check the issues page.