microsoft/qdk

Public

mirrored from https://github.com/microsoft/qdkAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
joaoboechat/remove-web-worker-dependency

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

source/npm/qsharp/src/workers/worker.ts

262lines · modecode

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4import * as wasm from "../../lib/web/qsc_wasm.js";
5import { QdkDiagnostics } from "../diagnostics.js";
6import { TelemetryEvent, log } from "../log.js";
7import type {
8 CommonEventMessage,
9 EventMessage,
10 IServiceEventMessage,
11 IServiceEventTarget,
12 MethodMap,
13 RequestMessage,
14 ResponseMessage,
15 ServiceMethods,
16 ServiceProtocol,
17} from "./types.js";
18
19type Wasm = typeof wasm;
20
21/**
22 * Creates an initializes a service, setting it up to receive requests.
23 * This function to be is used in the worker.
24 *
25 * @param serviceProtocol An object that describes the service: its constructor, methods and events
26 * @returns A message handler to be assigned to the `self.onmessage` handler in a web worker
27 */
28export function createWorker<
29 TService extends ServiceMethods<TService>,
30 TServiceEventMsg extends IServiceEventMessage,
31>(
32 serviceProtocol: ServiceProtocol<TService, TServiceEventMsg>,
33): (e: MessageEvent) => void {
34 let invokeService: ((req: RequestMessage<TService>) => Promise<void>) | null =
35 null;
36
37 // This export should be assigned to 'self.onmessage' in a WebWorker
38 return function messageHandler(e: MessageEvent) {
39 const data = e.data;
40
41 if (!data.type || typeof data.type !== "string") {
42 log.error(`Unrecognized msg: ${data}`);
43 return;
44 }
45
46 switch (data.type) {
47 case "init":
48 {
49 wasm.initSync({ module: data.wasmModule });
50
51 invokeService = initService<TService, TServiceEventMsg>(
52 self.postMessage.bind(self),
53 serviceProtocol,
54 wasm,
55 data.qscLogLevel,
56 );
57 }
58 break;
59 default:
60 if (!invokeService) {
61 log.error(
62 `Received message before the service was initialized: %o`,
63 data,
64 );
65 } else {
66 invokeService(data);
67 }
68 }
69 };
70}
71
72/**
73 * Serializes an error, if it is a known type, so that it can be sent between threads.
74 *
75 * By default, browsers can only send certain types of errors between the main thread and a worker.
76 * See: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm#error_types
77 *
78 * Serializing our own custom errors allows us to send them between threads.
79 */
80function serializeIfError(err: unknown) {
81 if (err instanceof QdkDiagnostics) {
82 err = { name: err.name, data: err.diagnostics };
83 } else if (err instanceof WebAssembly.RuntimeError) {
84 err = {
85 name: "WebAssembly.RuntimeError",
86 message: err.message,
87 stack: err.stack,
88 };
89 }
90 return err;
91}
92
93/**
94 * Function to wrap a service in a dispatcher. To be used in the worker thread.
95 *
96 * @param service The service to be wrapped
97 * @param methods A map of method names. Should match the list passed into @see createProxyInternal.
98 * @param eventNames The list of event names that the service can emit
99 * @param postMessage A function to post messages back to the main thread
100 * @returns A function that takes a message and invokes the corresponding
101 * method on the service. The caller should then set this method as a message handler.
102 */
103function createDispatcher<
104 TService extends ServiceMethods<TService>,
105 TServiceEventMsg extends IServiceEventMessage,
106>(
107 postMessage: (
108 msg: ResponseMessage<TService> | EventMessage<TServiceEventMsg>,
109 ) => void,
110 service: TService,
111 methods: MethodMap<TService>,
112 eventNames: TServiceEventMsg["type"][],
113): (req: RequestMessage<TService>) => Promise<void> {
114 log.trace("Worker: Constructing WorkerEventHandler");
115
116 function logAndPost(
117 msg: ResponseMessage<TService> | EventMessage<TServiceEventMsg>,
118 ) {
119 log.trace(
120 "Worker: Sending %s message from worker: %o",
121 msg.messageType,
122 msg,
123 );
124 postMessage(msg);
125 }
126
127 const eventTarget =
128 new EventTarget() as IServiceEventTarget<TServiceEventMsg>;
129
130 eventNames.forEach((eventName: TServiceEventMsg["type"]) => {
131 // Subscribe to all known events and forward them as messages to the main thread.
132 eventTarget.addEventListener(eventName, (ev) => {
133 logAndPost({
134 messageType: "event",
135 type: ev.type,
136 detail: ev.detail,
137 });
138 });
139
140 // If there's an addEventListener on the object itself, forward those events as well.
141 if ((service as any).addEventListener) {
142 (service as any).addEventListener(eventName, (ev: any) => {
143 logAndPost({
144 messageType: "event",
145 type: ev.type,
146 detail: ev.detail,
147 });
148 });
149 }
150 });
151
152 return function invokeMethod(req: RequestMessage<TService>) {
153 // Pass the eventTarget to the methods marked as taking progress
154 return service[req.type]
155 .call(
156 service,
157 ...req.args,
158 methods[req.type] === "requestWithProgress" ? eventTarget : undefined,
159 )
160 .then((result: any) =>
161 logAndPost({
162 messageType: "response",
163 type: req.type,
164 result: { success: true, result },
165 }),
166 )
167 .catch((err: any) => {
168 // Serialize the error if it's a known type.
169 err = serializeIfError(err);
170
171 logAndPost({
172 // If this happens then the wasm code likely threw an exception/panicked rather than
173 // completing gracefully and fulfilling the promise. Communicate to the client
174 // that there was an error and it should reject the current request
175 messageType: "response",
176 type: req.type,
177 result: { success: false, error: err },
178 });
179 });
180 };
181}
182
183/**
184 * Creates and initializes the actual service. To be used in the worker thread.
185 *
186 * @param postMessage A function to post messages back to the main thread
187 * @param serviceProtocol An object that describes the service: its constructor, methods and events
188 * @param wasm The wasm module to initialize the service with
189 * @param qscLogLevel The log level to initialize the service with
190 * @returns A function that takes a message and invokes the corresponding
191 * method on the service. The caller should then set this method as a message handler.
192 */
193function initService<
194 TService extends ServiceMethods<TService>,
195 TServiceEventMsg extends IServiceEventMessage,
196>(
197 postMessage: (
198 msg:
199 | ResponseMessage<TService>
200 | EventMessage<TServiceEventMsg>
201 | CommonEventMessage,
202 ) => void,
203 serviceProtocol: ServiceProtocol<TService, TServiceEventMsg>,
204 wasm: Wasm,
205 qscLogLevel?: number,
206): (req: RequestMessage<TService>) => Promise<void> {
207 function postTelemetryMessage(telemetry: TelemetryEvent) {
208 postMessage({
209 messageType: "common-event",
210 type: "telemetry-event",
211 detail: telemetry,
212 });
213 }
214
215 function postLogMessage(level: number, target: string, ...args: any) {
216 if (log.getLogLevel() < level) {
217 return;
218 }
219
220 let data = args;
221 try {
222 // Only structured cloneable objects can be sent in worker messages.
223 // Test if this is the case.
224 structuredClone(args);
225 } catch {
226 // Uncloneable object.
227 // Use String(args) instead of ${args} to handle all possible values
228 // without throwing. See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String#string_coercion
229 data = ["unsupported log data " + String(args)];
230 }
231 postMessage({
232 messageType: "common-event",
233 type: "log",
234 detail: { level, target, data },
235 });
236 }
237
238 // Override the global logger
239 log.error = (...args) => postLogMessage(1, "worker", ...args);
240 log.warn = (...args) => postLogMessage(2, "worker", ...args);
241 log.info = (...args) => postLogMessage(3, "worker", ...args);
242 log.debug = (...args) => postLogMessage(4, "worker", ...args);
243 log.trace = (...args) => postLogMessage(5, "worker", ...args);
244
245 if (qscLogLevel !== undefined) {
246 log.setLogLevel(qscLogLevel);
247 }
248
249 // Set up logging and telemetry as soon as possible after instantiating
250 log.onLevelChanged = (level) => wasm.setLogLevel(level);
251 log.setTelemetryCollector(postTelemetryMessage);
252 wasm.initLogging(postLogMessage, log.getLogLevel());
253
254 // Create the actual service and return the dispatcher method
255 const service = new serviceProtocol.class(wasm);
256 return createDispatcher(
257 postMessage,
258 service,
259 serviceProtocol.methods,
260 serviceProtocol.eventNames,
261 );
262}
263