LCOV - code coverage report

Current view
top level - /src/browser - _channel.dart
Test
lcov.info
Date
2022-04-02
Legend
Lines
hit
not hit
Branches
taken
not taken
# not executed
HitTotalCoverage
Lines11514281.0%
Functions00-
Branches00-
Each row represents a line of source code
LineBranchHitsSource code
1import 'dart:async';
2import 'dart:html' as web;
3
4import '../cancellation_token.dart';
5import '../channel.dart' show Channel, WorkerChannel;
6import '../squadron.dart';
7import '../squadron_exception.dart';
8import '../worker_exception.dart';
9import '../worker_request.dart';
10import '../worker_response.dart';
11
12class _MessagePort {
13 /// [web.MessagePort] to communicate with the [web.Worker] if the channel is owned by the worker owner.
14 /// Otherwise, [web.MessagePort] to return values to the client.
15 web.MessagePort? _sendPort;
16
17 /// [Channel] serialization in JavaScript world returns the [web.MessagePort].
181 dynamic serialize() => _sendPort;
19
201 void _postRequest(WorkerRequest req) {
211 final message = req.serialize();
221 try {
231 final transfer = _getTransferables(message).toList();
241 _sendPort!.postMessage(message, transfer);
25 } catch (ex) {
261 Squadron.severe('failed to post request $message: error $ex');
271 rethrow;
28 }
291 }
30
311 void _postResponse(WorkerResponse res) {
321 final message = res.serialize();
330 try {
341 final transfer = _getTransferables(message).toList();
351 _sendPort!.postMessage(message, transfer);
36 } catch (ex) {
370 Squadron.severe('failed to post response $message: error $ex');
380 rethrow;
39 }
401 }
41}
42
43/// [Channel] implementation for the JavaScript world.
44class _JsChannel extends _MessagePort implements Channel {
451 _JsChannel._();
46
47 /// [Channel] sharing in JavaScript world returns a [_JsForwardChannel].
48 @override
491 Channel share() => _JsForwardChannel._(_sendPort!);
50
51 /// Sends a termination [WorkerRequest] to the [web.Worker] and clears the [web.MessagePort].
52 @override
531 FutureOr close() {
541 if (_sendPort != null) {
551 _postRequest(WorkerRequest.stop());
561 _sendPort = null;
57 }
581 }
59
60 /// If the [token] is cancelled, sends a [WorkerRequest.cancel] message to signal the worker that the token is
61 /// cancelled.
62 @override
63 void notifyCancellation(CancellationToken token) {
64 if (token.cancelled) {
651 _postRequest(WorkerRequest.cancel(token));
66 }
67 }
68
69 /// Creates a [web.MessageChannel] and a [WorkerRequest] and sends it to the [web.Worker].
70 /// This method expects a single value from the [web.Worker].
71 @override
721 Future<T> sendRequest<T>(int command, List args,
73 {CancellationToken? token}) async {
741 final com = web.MessageChannel();
75 try {
761 _postRequest(WorkerRequest(com.port2, command, args, token));
771 final event = await com.port1.onMessage.first;
781 final res = WorkerResponse.deserialize(event.data);
791 return res.result as T;
80 } finally {
811 com.port2.close();
821 com.port1.close();
83 }
841 }
85
86 /// Creates a [web.MessageChannel] and a [WorkerRequest] and sends it to the [web.Worker].
87 /// This method expects a stream of values from the [web.Worker].
88 /// The [web.Worker] must send a [WorkerResponse.endOfStream] to close the [Stream].
89 @override
901 Stream<T> sendStreamingRequest<T>(int command, List args,
91 {CancellationToken? token}) {
921 final controller = StreamController<T>();
93 final com = web.MessageChannel();
941 com.port1.onMessage.listen((event) {
951 final res = WorkerResponse.deserialize(event.data);
96 if (res.endOfStream) {
971 controller.close();
981 com.port2.close();
991 com.port1.close();
1001 } else if (res.hasError) {
1011 controller.addError(res.error!, res.error!.stackTrace);
1021 controller.close();
1031 com.port2.close();
1041 com.port1.close();
105 } else {
1061 controller.add(res.result);
107 }
1081 });
1091 _postRequest(WorkerRequest(com.port2, command, args, token));
1101 return controller.stream;
1111 }
112}
113
114/// [WorkerChannel] implementation for the JavaScript world.
115class _JsWorkerChannel extends _MessagePort implements WorkerChannel {
1161 _JsWorkerChannel._();
117
118 /// Sends the [web.MessagePort] to communicate with the [web.Worker].
119 /// This method must be called by the [web.Worker] upon startup.
120 @override
121 void connect(Object channelInfo) {
122 if (channelInfo is web.MessagePort) {
123 reply(channelInfo);
124 } else {
125 throw WorkerException(
126 'invalid channelInfo ${channelInfo.runtimeType}: MessagePort expected');
127 }
128 }
129
130 /// Sends a [WorkerResponse] with the specified data to the worker client.
131 /// This method must be called from the [web.Worker] only.
132 @override
1331 void reply(dynamic data) => _postResponse(WorkerResponse(data));
134
135 /// Sends a [WorkerResponse.closeStream] to the worker client.
136 /// This method must be called from the [web.Worker] only.
137 @override
1381 void closeStream() => _postResponse(WorkerResponse.closeStream);
139
140 /// Sends the [WorkerResponse] to the worker client.
141 /// This method must be called from the [web.Worker] only.
142 @override
1431 void error(SquadronException error) {
1441 Squadron.finer(() => 'replying with error: $error');
1451 _postResponse(WorkerResponse.withError(error));
1461 }
147}
148
149/// [Channel] used to communicate between [web.Worker]s.
150/// Creates a [web.MessageChannel] to receive commands on [web.MessageChannel.port2] and forwards them
151/// to the worker's [web.MessagePort] via [web.MessageChannel.port1].
152class _JsForwardChannel extends _JsChannel {
153 /// [remote] is the worker's [web.MessagePort]
1541 _JsForwardChannel._(web.MessagePort remote) : super._() {
1551 _remote = remote;
1561 _com.port1.onMessage.listen(_forward);
1571 _sendPort = _com.port2;
1581 }
159
160 /// [web.MessagePort] to the worker.
161 web.MessagePort? _remote;
162
163 /// [web.MessageChannel] used for forwarding messages.
164 final _com = web.MessageChannel();
165
166 /// Forwards [web.MessageEvent.data] to the worker.
1671 void _forward(web.MessageEvent e) {
1681 final message = e.data;
1690 try {
1701 final transfer = _getTransferables(message).toList();
1711 _remote!.postMessage(message, transfer);
172 } catch (ex) {
1730 Squadron.severe('failed to forward $message: error $ex');
1740 rethrow;
175 }
1761 }
177
178 /// Closes this [Channel], effectively stopping message forwarding.
179 @override
1800 void close() {
1810 _remote = null;
1820 _com.port1.close();
1830 }
184}
185
186/// Checks if [value] is a base type value or an object.
1871bool _isObject(dynamic value) =>
1881 value != null &&
189 value is! num &&
190 value is! bool &&
191 value is! String &&
1921 value is! List<num> &&
1931 value is! List<bool> &&
1941 value is! List<String>;
195
196/// Excludes base type values from [list].
1971Iterable<Object> _getObjects(Iterable list, Set<Object> seen) sync* {
1981 for (var o in list.where(_isObject)) {
1991 if (!seen.contains(o)) {
2001 seen.add(o);
2011 yield o as Object;
202 }
203 }
2041}
205
206/// Yields objects contained in JSON object [args] (a Map, a List, or a base type).
207/// Used to identify non-base type objects and provide them to [web.Worker.postMessage].
208/// [web.Worker.postMessage] will clone these objects -- essentially [web.MessagePort]s.
209/// The code makes no effort to ensure these objects really are transferable.
2101Iterable<Object> _getTransferables(dynamic args) sync* {
2111 if (_isObject(args)) {
2121 if (args is Map) args = args.values;
2131 if (args is! Iterable) {
2140 yield args as Object;
215 } else {
2161 final seen = <Object>{};
2171 final toBeInspected = <Object>[];
2181 toBeInspected.addAll(_getObjects(args, seen));
2191 var i = 0;
2201 while (i < toBeInspected.length) {
221 final arg = toBeInspected[i++];
2221 if (arg is Map) {
2231 toBeInspected.addAll(_getObjects(arg.values, seen));
2241 } else if (arg is Iterable) {
2251 toBeInspected.addAll(_getObjects(arg, seen));
226 } else {
2271 yield arg;
228 }
229 }
230 }
231 }
2321}
233
234/// Stub implementations
235
236int _counter = 0;
237String _getId() {
2381 _counter++;
239 return '${Squadron.id}.$_counter';
240}
241
242/// Starts a [web.Worker] using the [entryPoint] and sends a start [WorkerRequest] with [startArguments].
243/// The future completes after the [web.Worker]'s main program has provided the [web.MessagePort] via [_JsWorkerChannel.connect].
2441Future<Channel> openChannel(dynamic entryPoint, List startArguments) {
245 final completer = Completer<Channel>();
246 final channel = _JsChannel._();
247 final com = web.MessageChannel();
2481 final worker = web.Worker(entryPoint);
249 Squadron.config('created Web Worker #${worker.hashCode}');
2501 worker.onError.listen((event) {
251 String msg;
2520 if (event is web.ErrorEvent) {
253 final error = event;
254 msg =
2550 '$entryPoint => ${error.message} [${error.filename}(${error.lineno})]';
256 } else {
2570 msg = '$entryPoint: ${event.type} / $event';
258 }
2590 Squadron.severe('error in Web Worker #${worker.hashCode}: $msg');
2600 if (!completer.isCompleted) {
2610 completer.completeError(
262 WorkerException('error in Web Worker #${worker.hashCode}: $msg'));
2630 worker.terminate();
264 }
2650 });
266 final message =
2671 WorkerRequest.start(com.port2, _getId(), startArguments).serialize();
2680 try {
2691 final transfer = _getTransferables(message).toList();
2701 worker.postMessage(message, transfer);
271 } catch (ex) {
2720 com.port1.close();
2730 worker.terminate();
2740 Squadron.severe('failed to post connection request $message: error $ex');
2750 rethrow;
276 }
2771 com.port1.onMessage.listen((event) {
2781 com.port1.close();
2791 final response = WorkerResponse.deserialize(event.data);
280 SquadronException? error = response.error;
2811 if (error == null) {
2820 try {
2831 channel._sendPort = response.result;
2841 Squadron.config('connected to Web Worker #${worker.hashCode}');
2851 completer.complete(channel);
2860 } catch (ex, st) {
2870 error = SquadronException.from(error: ex, stackTrace: st);
288 }
289 }
2901 if (error != null) {
2911 worker.terminate();
292 Squadron.severe(
293 'connection to Web Worker #${worker.hashCode} failed: ${response.error}');
2941 completer.completeError(error, error.stackTrace);
295 }
2961 });
2971 return completer.future;
2981}
299
300/// Creates a [_JsChannel] from a [web.MessagePort].
301Channel? deserializeChannel(dynamic channelInfo) =>
3021 (channelInfo == null) ? null : (_JsChannel._().._sendPort = channelInfo);
303
304/// Creates a [_JsWorkerChannel] from a [web.MessagePort].
3051WorkerChannel? deserializeWorkerChannel(dynamic channelInfo) =>
3061 (channelInfo == null)
307 ? null
3081 : (_JsWorkerChannel._().._sendPort = channelInfo);
Choose Features