1 | | | import 'dart:async'; |
2 | | | import 'dart:js_interop'; |
3 | | |
|
4 | | | import 'package:logger/web.dart'; |
5 | | | import 'package:web/web.dart' as web; |
6 | | |
|
7 | | | import '../../channel.dart'; |
8 | | | import '../../exceptions/exception_manager.dart'; |
9 | | | import '../../exceptions/squadron_error.dart'; |
10 | | | import '../../exceptions/squadron_exception.dart'; |
11 | | | import '../../exceptions/worker_exception.dart'; |
12 | | | import '../../tokens/_squadron_cancelation_token.dart'; |
13 | | | import '../../typedefs.dart'; |
14 | | | import '../../worker/worker_channel.dart'; |
15 | | | import '../../worker/worker_request.dart'; |
16 | | | import '../../worker/worker_response.dart'; |
17 | | | import '../xplat/_disconnected_channel.dart'; |
18 | | | import '../xplat/_result_stream.dart'; |
19 | | | import '../xplat/_transferables.dart'; |
20 | | | import '_entry_point_uri.dart'; |
21 | | | import '_event_buffer.dart'; |
22 | | | import '_patch.dart'; |
23 | | | import '_uri_checker.dart'; |
24 | | |
|
25 | | | part '_channel_impl.dart'; |
26 | | |
|
27 | | | /// Stub implementations |
28 | | |
|
29 | | | /// Starts a [web.Worker] using the [entryPoint] and sends a start |
30 | | | /// [WorkerRequest] with [startArguments]. The future completes after the |
31 | | | /// [web.Worker]'s main program has provided the [web.MessagePort] via |
32 | | | /// [WorkerChannel.connect]. |
33 | | 10 | Future<Channel> openChannel( |
34 | | | EntryPoint entryPoint, |
35 | | | ExceptionManager exceptionManager, |
36 | | | Logger? logger, |
37 | | | List startArguments, [ |
38 | | | PlatformThreadHook? hook, |
39 | | | ]) async { |
40 | | 10 | final completer = Completer<Channel>(); |
41 | | | final ready = Completer<bool>(); |
42 | | | Channel? channel; |
43 | | |
|
44 | | | final com = web.MessageChannel(); |
45 | | 10 | final webEntryPoint = EntryPointUri.from(entryPoint); |
46 | | 10 | late web.Worker worker; |
47 | | |
|
48 | | 3 | void fail(SquadronException ex) { |
49 | | 3 | if (!ready.isCompleted) ready.completeError(ex); |
50 | | 3 | if (!completer.isCompleted) completer.completeError(ex); |
51 | | 3 | } |
52 | | |
|
53 | | 9 | void success(Channel channel) { |
54 | | 9 | if (!ready.isCompleted) { |
55 | | 0 | throw SquadronErrorExt.create('Invalid state: worker is not ready'); |
56 | | | } |
57 | | 9 | if (!completer.isCompleted) completer.complete(channel); |
58 | | 9 | } |
59 | | |
|
60 | | 3 | try { |
61 | | 10 | worker = web.Worker(webEntryPoint.uri.toJS); |
62 | | |
|
63 | | 2 | void $errorHandler(web.ErrorEvent? e) { |
64 | | 2 | final err = getError(e), error = SquadronErrorExt.create(err.toString()); |
65 | | 2 | logger?.e(() => 'Connection to Web Worker failed: $error'); |
66 | | 2 | fail(error); |
67 | | |
|
68 | | 2 | UriChecker.exists(entryPoint).then((found) { |
69 | | 2 | try { |
70 | | 2 | final msg = (e != null) |
71 | | 2 | ? '$entryPoint => ${err.runtimeType} $err [${e.filename}(${e.lineno})]' |
72 | | 0 | : '$entryPoint => ${err.runtimeType} $err'; |
73 | | 1 | logger?.e(() => 'Unhandled error from Web Worker: $msg.'); |
74 | | 0 | if (!found) { |
75 | | 2 | logger?.e(() => 'It seems no Web Worker lives at $entryPoint.'); |
76 | | | } |
77 | | | } catch (_) { |
78 | | | // ignore |
79 | | | } |
80 | | 2 | }); |
81 | | 2 | } |
82 | | |
|
83 | | 10 | worker.onerror = $errorHandler.toJS; |
84 | | 10 | worker.onmessageerror = $errorHandler.toJS; |
85 | | |
|
86 | | | final disconnected = DisconnectedChannel(exceptionManager, logger); |
87 | | |
|
88 | | 10 | worker.onmessage = (web.MessageEvent? e) { |
89 | | 10 | try { |
90 | | 10 | final response = WorkerResponseExt.from(getMessageEventData(e) as List); |
91 | | 10 | if (!response.unwrapInPlace(disconnected)) { |
92 | | 0 | return; |
93 | | | } |
94 | | |
|
95 | | | final error = response.error; |
96 | | 10 | if (error != null) { |
97 | | 2 | logger?.e(() => 'Connection to Web Worker failed: $error'); |
98 | | 2 | fail(error); |
99 | | 9 | } else if (!ready.isCompleted) { |
100 | | 2 | logger?.t('Web Worker is ready'); |
101 | | 9 | ready.complete(response.result); |
102 | | | } |
103 | | 0 | } catch (ex, st) { |
104 | | 0 | return fail(SquadronException.from(ex, st)); |
105 | | | } |
106 | | 10 | }.toJS; |
107 | | |
|
108 | | 10 | final res = await ready.future; |
109 | | | if (!res) { |
110 | | 0 | throw SquadronErrorExt.create('Web Worker is not ready'); |
111 | | | } |
112 | | |
|
113 | | | final startRequest = WorkerRequest.start(com.port2, startArguments); |
114 | | |
|
115 | | 9 | com.port1.onmessage = (web.MessageEvent e) { |
116 | | 9 | final response = WorkerResponseExt.from(getMessageEventData(e) as List); |
117 | | 9 | if (!response.unwrapInPlace(disconnected)) { |
118 | | 9 | return; |
119 | | | } |
120 | | |
|
121 | | | final error = response.error; |
122 | | 9 | if (error != null) { |
123 | | 2 | logger?.e(() => 'Connection to Web Worker failed: $error'); |
124 | | 2 | fail(error); |
125 | | | } else if (response.endOfStream) { |
126 | | 1 | logger?.w('Disconnecting from Isolate'); |
127 | | 1 | channel?.close(); |
128 | | 9 | } else if (!completer.isCompleted) { |
129 | | 2 | logger?.t('Connected to Web Worker'); |
130 | | 9 | channel = _WebChannel._(response.result, logger, exceptionManager); |
131 | | 9 | success(channel!); |
132 | | | } else { |
133 | | 2 | logger?.d(() => 'Unexpected response: $response'); |
134 | | | } |
135 | | 9 | }.toJS; |
136 | | |
|
137 | | 0 | try { |
138 | | 9 | final data = startRequest.wrapInPlace(); |
139 | | | final msg = data.jsify(); |
140 | | 9 | final transfer = Transferables.get(data); |
141 | | | if (transfer == null || transfer.isEmpty) { |
142 | | | worker.postMessage(msg); |
143 | | | } else { |
144 | | 9 | final jsTransfer = transfer.jsify() as JSArray; |
145 | | | worker.postMessage(msg, jsTransfer); |
146 | | | } |
147 | | 0 | } catch (ex, st) { |
148 | | 2 | logger?.e(() => 'Failed to post connection request $startRequest: $ex'); |
149 | | 0 | throw SquadronErrorExt.create( |
150 | | 0 | 'Failed to post connection request: $ex', st); |
151 | | | } |
152 | | |
|
153 | | 2 | try { |
154 | | 9 | final channel = await completer.future; |
155 | | 9 | await hook?.call(worker); |
156 | | 2 | logger?.t('Created Web Worker for $entryPoint'); |
157 | | 9 | return channel; |
158 | | | } catch (ex) { |
159 | | 2 | logger?.e(() => 'Connection to Isolate failed: $ex'); |
160 | | 2 | rethrow; |
161 | | | } |
162 | | 3 | } catch (ex, st) { |
163 | | 3 | ready.future.ignore(); |
164 | | 3 | completer.future.ignore(); |
165 | | 1 | logger?.t('Failed to create Web Worker for $entryPoint'); |
166 | | | com.port1.close(); |
167 | | | com.port2.close(); |
168 | | | worker.terminate(); |
169 | | 3 | throw SquadronException.from(ex, st); |
170 | | | } finally { |
171 | | | webEntryPoint.release(); |
172 | | | } |
173 | | 10 | } |
174 | | |
|
175 | | | /// Creates a [_WebChannel] from a [web.MessagePort]. |
176 | | | Channel? deserialize(PlatformChannel? channelInfo, |
177 | | | [Logger? logger, ExceptionManager? exceptionManager]) => |
178 | | | (channelInfo == null) |
179 | | | ? null |
180 | | | : _WebChannel._( |
181 | | | channelInfo, |
182 | | | logger, |
183 | | | exceptionManager ?? ExceptionManager(), |
184 | | | ); |