1 | | | import 'dart:isolate'; |
2 | | |
|
3 | | | import '_worker_monitor.dart'; |
4 | | | import 'squadron.dart'; |
5 | | | import 'squadron_error.dart'; |
6 | | | import 'squadron_exception.dart'; |
7 | | | import 'worker.dart'; |
8 | | | import 'worker_exception.dart'; |
9 | | | import 'worker_request.dart'; |
10 | | | import 'worker_service.dart'; |
11 | | |
|
12 | | | class WorkerRunner { |
13 | | 2 | WorkerRunner(this._monitor); |
14 | | |
|
15 | | 1 | factory WorkerRunner.use(WorkerService service) { |
16 | | 1 | final worker = WorkerRunner(null); |
17 | | 4 | worker._operations.addAll(service.operations); |
18 | | | return worker; |
19 | | | } |
20 | | |
|
21 | | 1 | final _operations = <int, CommandHandler>{}; |
22 | | | final WorkerMonitor? _monitor; |
23 | | |
|
24 | | | /// Called by the platform worker upon startup, in response to a start [WorkerRequest]. [channelInfo] is an opaque |
25 | | | /// object sent back from the platform worker to the Squadron [Worker] and used to communicate with the platform |
26 | | | /// worker. Typically, [channelInfo] would be a [SendPort] (native) or a [MessagePort] (browser). [initializer] |
27 | | | /// is called to build the [WorkerService] associated to the worker. The runner's [_operations] map will be |
28 | | | /// populated with operations from the service. |
29 | | 1 | Future connect( |
30 | | | Map? message, Object channelInfo, WorkerInitializer initializer) async { |
31 | | 1 | final startRequest = WorkerRequest.deserialize(message); |
32 | | 1 | final client = startRequest?.client; |
33 | | |
|
34 | | | if (startRequest == null) { |
35 | | 1 | throw newSquadronError('connection request expected'); |
36 | | | } else if (client == null) { |
37 | | 0 | throw newSquadronError('missing client for connection request'); |
38 | | | } |
39 | | |
|
40 | | | try { |
41 | | 1 | if (!startRequest.connect) { |
42 | | 0 | throw newSquadronError('connection request expected'); |
43 | | 2 | } else if (_operations.isNotEmpty) { |
44 | | 0 | throw newSquadronError('already connected'); |
45 | | | } |
46 | | |
|
47 | | 2 | Squadron.setId(startRequest.id!); |
48 | | 2 | Squadron.logLevel = startRequest.logLevel!; |
49 | | |
|
50 | | 1 | final init = initializer(startRequest); |
51 | | 2 | final operations = ((init is Future) ? await init : init).operations; |
52 | | 5 | if (operations.keys.where((k) => k <= 0).isNotEmpty) { |
53 | | 1 | throw newSquadronError( |
54 | | | 'invalid command identifier in service operations map; command ids must be > 0'); |
55 | | | } |
56 | | 2 | _operations.addAll(operations); |
57 | | 1 | client.connect(channelInfo); |
58 | | | } catch (e, st) { |
59 | | 2 | client.error(SquadronException.from(error: e, stackTrace: st)); |
60 | | | } |
61 | | | } |
62 | | |
|
63 | | | /// [WorkerRequest] handler dispatching commands aoocrding to the [_operations] map. |
64 | | 2 | void processMessage(Map message) async { |
65 | | 2 | Squadron.finest(() => 'processing request $message'); |
66 | | 1 | final request = WorkerRequest.deserialize(message); |
67 | | 1 | final client = request?.client; |
68 | | |
|
69 | | | if (request == null) { |
70 | | 0 | throw newSquadronError('invalid message'); |
71 | | 1 | } else if (request.terminate) { |
72 | | 2 | return _monitor?.terminate(); |
73 | | 1 | } else if (request.cancel) { |
74 | | 3 | return _monitor?.cancel(request.cancelToken!); |
75 | | | } else if (client == null) { |
76 | | 0 | throw newSquadronError('missing client for request: $request'); |
77 | | | } |
78 | | |
|
79 | | 4 | final tokenRef = _monitor?.begin(request) ?? WorkerMonitor.noTokenRef; |
80 | | 1 | var streaming = false; |
81 | | 1 | try { |
82 | | 1 | if (request.connect) { |
83 | | | // connection request must be handled beforehand |
84 | | 0 | throw newSquadronError('unexpected connection request: $message'); |
85 | | 3 | } else if (_operations.isEmpty) { |
86 | | | // commands are not available yet (maybe connect() wasn't called or awaited) |
87 | | 0 | throw WorkerException('worker service is not ready'); |
88 | | 1 | } else if (tokenRef.cancelled) { |
89 | | 0 | throw tokenRef.exception!; |
90 | | | } |
91 | | | // retrieve operation matching the request command |
92 | | 4 | final op = _operations[request.command]; |
93 | | | if (op == null) { |
94 | | 3 | throw WorkerException('unknown command: ${request.command}'); |
95 | | | } |
96 | | | // process |
97 | | 2 | dynamic result = op(request); |
98 | | 3 | result = (result is Future) ? await result : result; |
99 | | 3 | if (result is Stream && result is! ReceivePort) { |
100 | | | // stream values to the client |
101 | | 1 | streaming = true; |
102 | | | CancelledException? ex; |
103 | | 3 | await for (var res in result) { |
104 | | | if (ex != null) { |
105 | | 0 | throw ex; |
106 | | | } |
107 | | 1 | client.reply(res); |
108 | | 1 | ex = tokenRef.exception; |
109 | | | } |
110 | | | } else { |
111 | | | // send result to client |
112 | | 1 | client.reply(result); |
113 | | | } |
114 | | 1 | } catch (e, st) { |
115 | | 3 | client.error(SquadronException.from(error: e, stackTrace: st)); |
116 | | | } finally { |
117 | | | if (streaming) { |
118 | | | // ensure a closeStream response is sent to terminate streaming operations |
119 | | 1 | client.closeStream(); |
120 | | | } |
121 | | 2 | _monitor?.done(tokenRef); |
122 | | | } |
123 | | 1 | } |
124 | | | } |