├── LICENSE ├── README.md ├── example.dart ├── interface.dart ├── worker.dart ├── worker_async.dart ├── worker_io.dart └── worker_web.dart /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Gábor DEÁK JAHN 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flutter Isolate Web 2 | 3 | The title is a misnomer. Of course, there are no isolates in Flutter Web. What this code provides 4 | is a unified interface to isolates *and* web workers so that each platform can use its own. 5 | It's not a package on pub.dev and it won't be because you can't use it out of the box just like 6 | a regular package or plugin. You have to copy it into your own code and modify it to suit your needs. 7 | 8 | ## Dependencies 9 | 10 | It depends on: 11 | 12 | * [isolate_handler](https://pub.dev/packages/isolate_handler), this is what provides 13 | the isolates with communication already in place, 14 | 15 | * [js](https://pub.dev/packages/js), this is what provides the connection to JavaScript. 16 | 17 | ## Usage 18 | 19 | Create a worker first: 20 | 21 | ```dart 22 | final worker = BackgroundWorker(); 23 | ``` 24 | 25 | and start it when needed: 26 | 27 | ```dart 28 | worker.spawn( 29 | doWork, 30 | name: 'some-unique-name', 31 | onInitialized: onInitialized, 32 | onFromWorker: onReceive, 33 | ); 34 | ``` 35 | 36 | You can start any amount of workers, just give a unique name to all so that you can reference them later when sending 37 | or receiving messages. 38 | 39 | `doWork()` is a function taking a `Map` argument (the context of the isolate/worker). As customary with standard isolates, 40 | it has to be a top-level or static function. The most usual activity here is to start listening to messages the isolate/worker 41 | will receive from the main app (the actual message structure is completely up to you, this is just an example): 42 | 43 | ```dart 44 | void doWork(Map context) { 45 | worker.listen((args) { 46 | switch (args['command']) { 47 | case 'start': 48 | // worker starts its job 49 | break; 50 | } 51 | }, context: context); 52 | } 53 | ``` 54 | 55 | When the isolate/worker actually gets initialized, the main app will be notified. You might simply use this to send a message 56 | back to the worker to start the actual work (the actual message structure is completely up to you, this is just an example): 57 | 58 | ```dart 59 | void onInitialized() { 60 | worker.sendTo('some-unique-name', { 61 | // tell the worker to start its job 62 | 'command': 'start', 63 | 'data': ..., 64 | }); 65 | } 66 | ``` 67 | 68 | There is an important difference between the two that must be understood. `doWork()` runs in the worker/isolate, 69 | this is the main entry point of the worker/isolate code. `onInitialized` and `onFromWorker` run in the main app, 70 | this is where the main app receives messages from the workers/isolates. 71 | 72 | This second is the main messaging mechanism. Make sure the isolate/worker also knows the main `worker` object 73 | and its own unique name (the name was returned to the `entryPoint()` as `context['name']`) because it needs those 74 | to send its messages back: 75 | 76 | ```dart 77 | // isolate/worker sends to main app: 78 | worker.sendFrom('unique-name', message); 79 | 80 | // main app sends to isolate/worker: 81 | worker.sendTo('unique-name', message); 82 | 83 | // main app receives from isolate/worker: 84 | void onReceive(T message) { 85 | //... 86 | } 87 | ``` 88 | 89 | If you need to kill the worker/isolate, use: 90 | 91 | ```dart 92 | worker.kill('unique-name'); 93 | ``` 94 | 95 | To kill all of them, use the `names` list: 96 | 97 | ```dart 98 | for (String name in worker.names) worker.kill(name); 99 | ``` 100 | 101 | ## Web 102 | 103 | Everything described above works on mobile with isolates. However, the Flutter Web side is not that nice yet. 104 | 105 | If you have the JavaScript code you want to run when compiling your app for the web, it works. Call your code with 106 | the `importScripts()` in `worker_web.dart` and that's all. You can even do away with all that `createObjectUrlFromBlob` 107 | stuff and construct the `Worker` directly with the JS file from your app assets or downloaded from a CDN. The same applies 108 | if your isolate code from your mobile app is simple enough so that you can and want to replicate it in JavaScript. 109 | 110 | The missing link in the chain is if you want to use the *same* Dart code you used for your isolate in your web worker. 111 | If your code is free from dependencies, you could get away with simply compiling it with: 112 | 113 | dart2js --libraries-spec=$HOME/flutter/bin/cache/flutter_web_sdk/libraries.json -o worker.js worker.dart 114 | 115 | copying it to your assets and loading it. But we are unable to simply re-use our existing Dart code, even if it is actually 116 | compiled to JavaScript, anyway. There's no way to compile part of your app (your isolate/worker) into a separate file and 117 | to include it separately for the worker to use, and, by obvious limitation of web workers, we can't simply call into our 118 | main app code from the worker, either. 119 | 120 | ### Intermediate solution for the web 121 | 122 | The package also has a `worker_async.dart` file. This is an async solution to the problem. It implements the same interface, 123 | so it can be used as is to make the isolate/worker scheme work on the web. It won't be a real web worker, though, it won't run 124 | in parallel but it works. These "fake" workers will be called in the usual asnychronous way. While not yet the real McCoy, 125 | it's a functional replacement. 126 | -------------------------------------------------------------------------------- /example.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2020 DEÁK JAHN Gábor. 3 | * All rights reserved. 4 | */ 5 | 6 | final worker = BackgroundWorker(); 7 | int counter = 0; 8 | 9 | void main() { 10 | // Start the worker/isolate at the `entryPoint` function. 11 | worker.spawn(entryPoint, 12 | name: "counter", 13 | // Executed every time data is received from the spawned worker/isolate. 14 | onReceive: setCounter, 15 | // Executed once when spawned worker/isolate is ready for communication. 16 | onInitialized: () => worker.sendTo("counter", counter), 17 | ); 18 | } 19 | 20 | // Set new count and display current count. 21 | void setCounter(int count) { 22 | counter = count; 23 | print("Counter is now $counter"); 24 | 25 | // We will no longer be needing the worker/isolate, let's dispose of it. 26 | worker.kill("counter"); 27 | } 28 | 29 | // This function happens in the worker/isolate. 30 | void entryPoint(String name) { 31 | // Triggered every time data is received from the main app. 32 | worker.listen((count) { 33 | // Add one to the count and send the new value back to the main app. 34 | worker.sendFrom(name, ++count); 35 | }, name: name); 36 | } 37 | -------------------------------------------------------------------------------- /interface.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2020 DEÁK JAHN Gábor. 3 | * All rights reserved. 4 | */ 5 | 6 | library worker; 7 | 8 | import 'package:flutter/foundation.dart'; 9 | 10 | import 'worker.dart' // 11 | if (dart.library.io) 'worker_io.dart' 12 | if (dart.library.html) 'worker_web.dart'; 13 | 14 | abstract class BackgroundWorker { 15 | factory BackgroundWorker() => getWorker(); 16 | 17 | /// Returns the names of all running workers. 18 | List get names => []; 19 | 20 | /// Starts a new worker. 21 | /// 22 | /// [entryPoint] is the place for the actual work in the worker: start from here what you want to accomplish in the worker. 23 | /// It must be a top-level or static function, with a single argument [context]. [name] must be a unique name to refer 24 | /// to the worker later. [onInitialized] will be called when the worker is actually started and ready to send or receive messages. 25 | /// [onFromWorker] will be called with all messages coming from the worker. 26 | void spawn(void Function(Map) entryPoint, {@required String name, void Function() onInitialized, void Function(Map message) onFromWorker}); 27 | 28 | /// Sends a message to a worker. 29 | /// 30 | /// [name] identifies the worker to send the message to. 31 | void sendTo(String name, dynamic message); 32 | 33 | /// Sends a message from a worker. 34 | /// 35 | /// Workers can use this function to send their messages back to the main app. 36 | /// In order to do that, they must have a reference to this object (can be sent to them when they are started) 37 | /// and they also have to know their own uniqe [name]. 38 | void sendFrom(String name, dynamic message); 39 | 40 | /// Receives messages from the main app. 41 | /// 42 | /// Workers can use this function to set up their listener for messages coming from the main app. 43 | /// This is normally called from their [entryPoint] function, passing the [context] that function receives. 44 | void listen(void Function(dynamic message) onFromMain, {@required Map context, void Function() onError, void Function() onDone, bool cancelOnError}); 45 | 46 | /// Kills a worker. 47 | /// 48 | /// [name] identifies to the worker to kill. 49 | void kill(String name); 50 | } 51 | -------------------------------------------------------------------------------- /worker.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2020 DEÁK JAHN Gábor. 3 | * All rights reserved. 4 | * 5 | */ 6 | 7 | import 'interface.dart'; 8 | 9 | BackgroundWorker getWorker() => throw UnimplementedError('getWorker'); 10 | -------------------------------------------------------------------------------- /worker_async.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2020 DEÁK JAHN Gábor. 3 | * All rights reserved. 4 | */ 5 | 6 | import 'dart:async'; 7 | 8 | import 'package:cpc_mobile/order/uploader/worker/interface.dart'; 9 | import 'package:flutter/foundation.dart'; 10 | 11 | BackgroundWorker getWorker() => BackgroundWorkerAsync(); 12 | 13 | /// This is NOT a real background worker, it's a simple asynchronous one. 14 | /// It implements the same interface and sends and receives messages the same way as the real workers do, 15 | /// so it can be used for testing instead of a parallel worker (or when a real worker can't be built). 16 | class BackgroundWorkerAsync implements BackgroundWorker { 17 | final Map message)> _messengers = {}; 18 | final events = StreamController<_BackgroundWorkerEvent>.broadcast(); 19 | 20 | /// Returns the names of all running workers. 21 | @override 22 | List get names => _messengers.keys.toList(); 23 | 24 | /// Starts a new worker. 25 | /// 26 | /// [entryPoint] is the place for the actual work in the worker: start from here what you want to accomplish in the worker. 27 | /// It must be a top-level or static function, with a single argument [context]. [name] must be a unique name to refer 28 | /// to the worker later. [onInitialized] will be called when the worker is actually started and ready to send or receive messages. 29 | /// [onFromWorker] will be called with all messages coming from the worker. 30 | @override 31 | void spawn(void Function(Map) entryPoint, {@required String name, void Function() onInitialized, void Function(Map message) onFromWorker}) { 32 | assert(entryPoint != null); 33 | 34 | _messengers[name] = onFromWorker; 35 | entryPoint({'name': name}); 36 | onInitialized?.call(); 37 | } 38 | 39 | /// Sends a message to a worker. 40 | /// 41 | /// [name] identifies the worker to send the message to. 42 | @override 43 | void sendTo(String name, dynamic message) { 44 | events.add(_BackgroundWorkerEvent(name, message)); 45 | } 46 | 47 | /// Sends a message from a worker. 48 | /// 49 | /// Workers can use this function to send their messages back to the main app. 50 | /// In order to do that, they must have a reference to this object (can be sent to them when they are started) 51 | /// and they also have to know their own uniqe [name]. 52 | @override 53 | void sendFrom(String name, dynamic message) { 54 | final messenger = _messengers[name]; 55 | assert(messenger != null, 'Unknown name'); 56 | 57 | messenger(message); 58 | } 59 | 60 | /// Receives messages from the main app. 61 | /// 62 | /// Workers can use this function to set up their listener for messages coming from the main app. 63 | /// This is normally called from their [entryPoint] function, passing the [context] that function receives. 64 | @override 65 | void listen(void Function(dynamic message) onFromMain, {@required Map context, void Function() onError, void Function() onDone, bool cancelOnError}) { 66 | assert(onFromMain != null); 67 | 68 | String name = context['name']; 69 | assert(name != null, 'Unknown name'); 70 | 71 | _onEvent(name: name).listen((event) { 72 | onFromMain(event.message); 73 | }); 74 | 75 | if (onError != null) 76 | _onError(name: name).listen((event) { 77 | onError(); 78 | if (cancelOnError) kill(name); 79 | }); 80 | } 81 | 82 | /// Kills a worker. 83 | /// 84 | /// [name] identifies to the worker to kill. 85 | @override 86 | void kill(String name) {} 87 | 88 | /// Internal event queue for the worker. It stores *incoming* messages. 89 | Stream<_BackgroundWorkerMessage> _onEvent({@required String name}) { 90 | return events.stream // 91 | .where((event) => event.name == name && event is _BackgroundWorkerMessage) 92 | .cast<_BackgroundWorkerMessage>(); 93 | } 94 | 95 | /// Internal event queue for the worker. It stores error events. 96 | Stream<_BackgroundWorkerError> _onError({@required String name}) { 97 | return events.stream // 98 | .where((event) => event.name == name && event is _BackgroundWorkerError) 99 | .cast<_BackgroundWorkerError>(); 100 | } 101 | } 102 | 103 | class _BackgroundWorkerEvent { 104 | final String name; 105 | final T message; 106 | 107 | _BackgroundWorkerEvent(this.name, this.message); 108 | } 109 | 110 | class _BackgroundWorkerError extends _BackgroundWorkerEvent { 111 | _BackgroundWorkerError(String name) : super(name, null); 112 | } 113 | 114 | class _BackgroundWorkerMessage extends _BackgroundWorkerEvent> { 115 | _BackgroundWorkerMessage(String name, Map message) : super(name, message); 116 | } 117 | -------------------------------------------------------------------------------- /worker_io.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2020 DEÁK JAHN Gábor. 3 | * All rights reserved. 4 | */ 5 | 6 | import 'package:flutter/foundation.dart'; 7 | import 'package:isolate_handler/isolate_handler.dart'; 8 | 9 | import 'interface.dart'; 10 | 11 | BackgroundWorker getWorker() => BackgroundWorkerIo(); 12 | 13 | class BackgroundWorkerIo implements BackgroundWorker { 14 | final _isolates = IsolateHandler(); 15 | final Map _messengers = {}; 16 | 17 | /// Returns the names of all running workers. 18 | @override 19 | List get names => _isolates.isolates.keys.toList(); 20 | 21 | /// Starts a new worker. 22 | /// 23 | /// [entryPoint] is the place for the actual work in the worker: start from here what you want to accomplish in the worker. 24 | /// It must be a top-level or static function, with a single argument [context]. [name] must be a unique name to refer 25 | /// to the worker later. [onInitialized] will be called when the worker is actually started and ready to send or receive messages. 26 | /// [onFromWorker] will be called with all messages coming from the worker. 27 | @override 28 | void spawn(void Function(Map) entryPoint, {@required String name, void Function() onInitialized, void Function(Map message) onFromWorker}) { 29 | assert(entryPoint != null); 30 | 31 | _isolates.spawn( 32 | entryPoint, 33 | name: name, 34 | onInitialized: onInitialized, 35 | onReceive: onFromWorker, 36 | ); 37 | } 38 | 39 | /// Sends a message to a worker. 40 | /// 41 | /// [name] identifies the worker to send the message to. 42 | @override 43 | void sendTo(String name, dynamic message) { 44 | _isolates.send(message, to: name); 45 | } 46 | 47 | /// Sends a message from a worker. 48 | /// 49 | /// Workers can use this function to send their messages back to the main app. 50 | /// In order to do that, they must have a reference to this object (can be sent to them when they are started) 51 | /// and they also have to know their own uniqe [name]. 52 | @override 53 | void sendFrom(String name, dynamic message) { 54 | final messenger = _messengers[name]; 55 | assert(messenger != null, 'Unknown name'); 56 | 57 | messenger.send(message); 58 | } 59 | 60 | /// Receives messages from the main app. 61 | /// 62 | /// Workers can use this function to set up their listener for messages coming from the main app. 63 | /// This is normally called from their [entryPoint] function, passing the [context] that function receives. 64 | @override 65 | void listen(void Function(dynamic message) onFromMain, {@required Map context, void Function() onError, void Function() onDone, bool cancelOnError}) { 66 | assert(onFromMain != null); 67 | 68 | String name = context['name']; 69 | final messenger = _messengers[name] = HandledIsolate.initialize(context); 70 | messenger.listen( 71 | onFromMain, 72 | onError: onError, 73 | onDone: onDone, 74 | cancelOnError: cancelOnError, 75 | ); 76 | } 77 | 78 | /// Kills a worker. 79 | /// 80 | /// [name] identifies to the worker to kill. 81 | @override 82 | void kill(String name) { 83 | _isolates.kill(name); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /worker_web.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2020 DEÁK JAHN Gábor. 3 | * All rights reserved. 4 | */ 5 | 6 | // https://github.com/flutter/flutter/issues/33577 7 | 8 | import 'dart:html'; 9 | 10 | import 'package:flutter/foundation.dart'; 11 | 12 | import 'interface.dart'; 13 | 14 | BackgroundWorker getWorker() => BackgroundWorkerWeb(); 15 | 16 | class BackgroundWorkerWeb implements BackgroundWorker { 17 | final Map _workers = {}; 18 | final Map _workerUrls = {}; 19 | final Map message)> _messengers = {}; 20 | 21 | static String source = 22 | "importScripts(location.origin + '/sample.js');// entryPoint();"; 23 | 24 | /// Returns the names of all running workers. 25 | @override 26 | List get names => _workers.keys.toList(); 27 | 28 | /// Starts a new worker. 29 | /// 30 | /// [entryPoint] is the place for the actual work in the worker: start from here what you want to accomplish in the worker. 31 | /// It must be a top-level or static function, with a single argument [context]. [name] must be a unique name to refer 32 | /// to the worker later. [onInitialized] will be called when the worker is actually started and ready to send or receive messages. 33 | /// [onFromWorker] will be called with all messages coming from the worker. 34 | @override 35 | void spawn(void Function(Map) entryPoint, {@required String name, void Function() onInitialized, void Function(Map message) onFromWorker}) { 36 | assert(entryPoint != null); 37 | 38 | final code = Blob([source], 'text/javascript'); 39 | String codeUrl = _workerUrls[name] = Url.createObjectUrlFromBlob(code); 40 | final worker = _workers[name] = Worker(codeUrl); 41 | _messengers[name] = onFromWorker; 42 | worker.onMessage.listen((event) { 43 | final args = Map.from(event.data); 44 | onFromWorker?.call(args); 45 | }); 46 | onInitialized?.call(); 47 | } 48 | 49 | /// Sends a message to a worker. 50 | /// 51 | /// [name] identifies the worker to send the message to. 52 | @override 53 | void sendTo(String name, dynamic message) { 54 | final worker = _workers[name]; 55 | assert(worker != null, 'Unknown name'); 56 | 57 | worker.postMessage(message); 58 | } 59 | 60 | /// Sends a message from a worker. 61 | /// 62 | /// Workers can use this function to send their messages back to the main app. 63 | /// In order to do that, they must have a reference to this object (can be sent to them when they are started) 64 | /// and they also have to know their own uniqe [name]. 65 | @override 66 | void sendFrom(String name, dynamic message) { 67 | final messenger = _messengers[name]; 68 | assert(messenger != null, 'Unknown name'); 69 | 70 | messenger(message); 71 | } 72 | 73 | /// Receives messages from the main app. 74 | /// 75 | /// Workers can use this function to set up their listener for messages coming from the main app. 76 | /// This is normally called from their [entryPoint] function, passing the [context] that function receives. 77 | @override 78 | void listen(void Function(dynamic message) onFromMain, 79 | {@required Map context, 80 | void Function() onError, 81 | void Function() onDone, 82 | bool cancelOnError}) { 83 | assert(onFromMain != null); 84 | 85 | String name = context['name']; 86 | final worker = _workers[name]; 87 | assert(worker != null, 'Unknown name'); 88 | 89 | if (onError != null) 90 | worker.onError.listen((event) { 91 | onError(); 92 | if (cancelOnError) kill(name); 93 | }); 94 | } 95 | 96 | /// Kills a worker. 97 | /// 98 | /// [name] identifies to the worker to kill. 99 | @override 100 | void kill(String name) { 101 | final worker = _workers[name]; 102 | assert(worker != null, 'Unknown name'); 103 | 104 | worker.terminate(); 105 | Url.revokeObjectUrl(_workerUrls[name]); 106 | } 107 | } 108 | --------------------------------------------------------------------------------