LCOV - code coverage report

Current view
top level - src/_impl/xplat - _worker_runner.dart
Test
lcov.info
Date
2026-11-07
Legend
Lines
hit
not hit
Branches
taken
not taken
# not executed
HitTotalCoverage
Lines12814290.1%
Functions00-
Branches00-
Each row represents a line of source code
LineBranchHitsSource code
1import 'dart:async';
2
3import 'package:logger/web.dart';
4
5import '../../build_options.dart';
6import '../../exceptions/squadron_error.dart';
7import '../../exceptions/squadron_exception.dart';
8import '../../local_worker/local_worker.dart';
9import '../../service_installer.dart';
10import '../../tokens/_cancelation_token_ref.dart';
11import '../../tokens/_squadron_cancelation_token.dart';
12import '../../typedefs.dart';
13import '../../worker/worker_channel.dart';
14import '../../worker/worker_message.dart';
15import '../../worker/worker_request.dart';
16import '../../worker_service.dart';
17import '_internal_logger.dart';
18
19class WorkerRunner {
20 /// Constructs a new worker runner.
211 WorkerRunner(this._terminate);
22
23 void Function(WorkerRunner) _terminate;
24
25 final internalLogger =
26 BuildOptions.withInternalLogging ? InternalLogger() : null;
27
28 WorkerService? _service;
29 OperationsMap? _operations;
30
31 final _cancelTokens = <String, CancelationTokenReference>{};
32
33 bool _terminationRequested = false;
34 int _executing = 0;
35
36 final _streamCancelers = <StreamId, StreamCanceler>{};
37
38 void Function(OutputEvent)? _logForwarder;
39
40 /// Constructs a new worker runner for a [localWorker].
411 factory WorkerRunner.local(LocalWorker localWorker) {
421 final runner = WorkerRunner((r) {
430 r.internalLogger?.t('Terminating local Worker');
440 r._service = null;
450 r._operations = null;
46 });
472 _checkOperations(localWorker.operations);
481 runner._service = localWorker;
492 runner._operations = localWorker.operations;
50 return runner;
51 }
52
531 static void _checkOperations(OperationsMap ops) {
545 final invalidKeys = ops.keys.where((k) => k <= 0).toList();
551 if (invalidKeys.isNotEmpty) {
561 throw SquadronErrorImpl.create(
574 'Invalid command identifier${(invalidKeys.length > 1) ? 's' : ''} in service operations map: ${invalidKeys.join(', ')}. Command ids must be positive.',
58 );
59 }
60 }
61
62 /// Called by the platform worker upon startup, in response to a start
63 /// [WorkerRequest]. [channelInfo] is an opaque object sent back from the
64 /// platform worker to the Squadron [Worker] and used to communicate with the
65 /// platform worker. Typically, [channelInfo] would be a [SendPort] (native)
66 /// or a [MessagePort] (browser). [initializer] is called to build the
67 /// [WorkerService] associated to the worker. The runner's [_service] map
68 /// will be set with operations from the service.
691 void connect(WorkerRequest? startRequest, PlatformChannel channelInfo,
70 WorkerInitializer initializer) async {
71 late final WorkerChannel? channel;
72 try {
732 startRequest?.unwrapInPlace(internalLogger);
741 channel = startRequest?.channel;
75
76 if (startRequest == null) {
771 throw SquadronErrorImpl.create('Missing connection request');
78 } else if (channel == null) {
790 throw SquadronErrorImpl.create('Missing client for connection request');
80 }
81
821 if (BuildOptions.withCrossWorkerLogging && _logForwarder == null) {
831 final logger = channel.log;
844 _logForwarder = (event) => logger(event.origin);
852 Logger.addOutputListener(_logForwarder!);
86 }
87
881 if (!startRequest.isConnection) {
890 throw SquadronErrorImpl.create('Connection request expected');
902 } else if (_service != null || _operations != null) {
910 throw SquadronErrorImpl.create('Already connected');
92 }
93
941 var service = initializer(startRequest);
951 if (service is Future<WorkerService>) service = await service;
96 service as WorkerService;
972 _checkOperations(service.operations);
981 _service = service;
992 _operations = service.operations;
100
1011 channel.connect(channelInfo);
102
1031 if (service is ServiceInstaller) {
1042 _installCompleter = Completer<void>();
105 try {
1061 final result = (service as ServiceInstaller).install();
1071 if (result is Future) await result;
108 } catch (ex, st) {
1094 internalLogger?.e(() => 'Service installation failed: $ex');
1101 channel.error(ex, st);
1111 channel.closeStream();
1122 _installError = SquadronException.from(ex, st);
113 } finally {
1142 _installCompleter?.complete();
115 }
116 }
117 } catch (ex, st) {
1184 internalLogger?.e(() => 'Connection failed: $ex');
1191 channel?.error(ex, st);
1201 _exit();
121 }
122 }
123
124 Completer<void>? _installCompleter;
125 SquadronException? _installError;
126
127 /// [WorkerRequest] handler dispatching commands according to the
128 /// [_service] map. Make sure this method doesn't throw.
1291 void processRequest(WorkerRequest request) async {
130 WorkerChannel? channel;
131 try {
1322 request.unwrapInPlace(internalLogger);
1331 channel = request.channel;
134
1351 if (request.isTermination) {
136 // terminate the worker
1371 return _shutdown();
138 }
139
140 // check installation result if necessary
1412 final pendingInstallation = _installCompleter?.future;
142 if (pendingInstallation != null) {
143 await pendingInstallation;
1441 _installCompleter = null;
145 }
146
1471 if (_installError != null) {
148 // service installation failed
1491 throw _installError!;
150 }
151
152 // ==== these requests do not send a response ====
153
1541 if (request.isTokenCancelation) {
155 // cancel a token
1561 final token = request.cancelToken!;
1572 return _getTokenRef(token).update(token);
1581 } else if (request.isStreamCancelation) {
159 // cancel a stream
1603 final canceler = _streamCancelers[request.streamId];
1611 return canceler?.call();
162 }
163
164 // make sure the worker is connected
165
1661 if (request.isConnection) {
167 // connection requests are handled by connect().
1680 throw SquadronErrorImpl.create(
1690 'Unexpected connection request: $request');
170 }
171
172 // find the operation matching the request command
1733 final cmd = request.command, op = _operations![cmd];
174 if (op == null) {
175 // unknown command, or commands are not available yet (maybe connect() wasn't called or awaited)
1762 throw SquadronErrorImpl.create((_operations == null)
177 ? 'Worker service is not ready'
1781 : 'Unknown command: $cmd');
179 }
180
181 // ==== other requests require a client to send the response ====
182
183 if (channel == null) {
1840 throw SquadronErrorImpl.create('Missing client for request: $request');
185 }
186
1871 final token = request.cancelToken;
1881 token?.throwIfCanceled();
189
190 // start monitoring execution
1911 final tokenRef = _begin(request);
192 try {
193 // process
1941 var result = op(request);
1951 if (result is Future) {
196 result = await result;
197 }
198
1991 final reply = request.reply!;
2002 if (result is Stream && channel.canStream(result)) {
201 // result is a stream: forward data to the client
2021 final replyWithError = channel.error;
2031 void $postError(Object exception, [StackTrace? stackTrace]) {
2041 replyWithError(exception, stackTrace, cmd);
205 }
206
2071 void post(data) {
208 try {
2091 reply(data);
210 } catch (ex, st) {
2110 $postError(ex, st);
212 }
213 }
214
2151 await _pipe(result, channel, post, $postError, token);
216 } else {
217 // result is a value: send to the client
2181 reply(result);
219 }
220 } finally {
221 // stop monitoring execution
2221 _done(tokenRef);
223 }
224 } catch (ex, st) {
225 if (channel != null) {
2262 channel.error(ex, st, request.command);
227 } else {
2283 internalLogger?.e('Unhandled error: $ex');
229 }
230 }
231 }
232
2331 CancelationTokenReference _getTokenRef(SquadronCancelationToken? token) =>
234 (token == null)
2351 ? CancelationTokenReference.noToken
2362 : _cancelTokens.putIfAbsent(
2374 token.id, () => CancelationTokenReference(token.id));
238
239 /// Starts monitoring execution of this [request]. If the request contains a
240 /// cancelation token, it is overridden with a [CancelationTokenReference]
241 /// and this reference is returned to the sender. Otherwise, returns
242 /// [CancelationTokenReference.noToken].
2431 CancelationTokenReference _begin(WorkerRequest request) {
2442 _executing++;
2452 final token = _getTokenRef(request.cancelToken);
2461 token.usedBy(request);
247 return token;
248 }
249
250 /// Stops monitoring execution and releases the [tokenRef].
2511 void _done(CancelationTokenReference tokenRef) {
2521 tokenRef.release();
2532 if (tokenRef.refCount == 0) {
2543 _cancelTokens.remove(tokenRef.id);
255 }
2562 _executing--;
2573 if (_terminationRequested && _executing == 0) {
2581 _unmount();
259 }
260 }
261
262 /// Forwards stream events to client.
2631 Future<void> _pipe(
264 Stream<dynamic> stream,
265 WorkerChannel channel,
266 void Function(dynamic) post,
267 void Function(Object exception, [StackTrace? stackTrace]) postError,
268 SquadronCancelationToken? token,
269 ) {
270 late final StreamSubscription subscription;
2711 final done = Completer();
272
273 late final StreamId streamId;
274
275 // send endOfStream to client
2761 Future<void> onDone() {
2771 _unregisterStreamCanceler(streamId);
2781 channel.closeStream();
2793 return subscription.cancel().whenComplete(done.complete);
280 }
281
282 final bool Function() checkToken;
283 if (token == null) {
2841 checkToken = () => true;
285 } else {
2861 checkToken = () {
2871 final ex = token.exception;
288 if (ex != null) {
2890 postError(ex);
2900 onDone();
291 }
292 return (ex == null);
293 };
294 }
295
296 // register stream canceler callback and connect stream with client
2971 streamId = _registerStreamCanceler(onDone);
2981 post(streamId);
2991 if (checkToken()) {
300 // start forwarding messages to the client
3011 subscription = stream.listen(
3021 (data) {
3032 if (checkToken()) post(data);
304 },
3051 onError: (ex, st) {
3062 if (checkToken()) postError(ex, st);
307 },
308 onDone: onDone,
309 cancelOnError: false,
310 );
311 }
312
3131 return done.future;
314 }
315
316 /// Assigns a stream ID to the stream canceler callback and registers the
317 /// callback.
3181 StreamId _registerStreamCanceler(StreamCanceler canceler) {
3191 final streamId = StreamId.next();
3202 _streamCancelers[streamId] = canceler;
321 return streamId;
322 }
323
324 /// Unregisters the stream canceled callback associated to the [streamId].
3251 void _unregisterStreamCanceler(StreamId streamId) {
3262 _streamCancelers.remove(streamId);
327 }
328
329 /// Terminates the worker if there is no pending execution. Otherwise, marks
330 /// the worker as terminating and termination will be effective when all
331 /// pending executions have completed.
3321 void _shutdown() {
3331 _terminationRequested = true;
3342 if (_executing == 0) {
3351 _unmount();
336 }
337 }
338
339 // should not throw
3401 void _unmount() async {
341 try {
342 // uninstall the service if necessary
3432 if (_service is ServiceInstaller) {
344 // check installation result
3451 final pendingInstallation = _installCompleter?.future;
346 if (pendingInstallation != null) {
347 await pendingInstallation;
3480 _installCompleter = null;
349 }
3501 if (_installError == null) {
351 // uninstall iif the service installed succesfuly
3522 await (_service as ServiceInstaller).uninstall();
353 }
354 }
355 } catch (ex) {
3563 internalLogger?.e('Service uninstallation failed with error: $ex');
357 } finally {
3581 _exit();
359 }
360 }
361
362 // should not throw
3631 void _exit() {
364 try {
3652 _terminate(this);
366 } catch (ex) {
3670 internalLogger?.e('Worker termination failed with error: $ex');
368 }
3691 if (BuildOptions.withCrossWorkerLogging && _logForwarder != null) {
3702 Logger.removeOutputListener(_logForwarder!);
371 }
372 }
373}
Choose Features