microsoft/qdk

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
fedimser/sparsesim-version

Branches

Tags

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

Clone

HTTPS

Download ZIP

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

362lines · modecode

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4import type { IQSharpError } from "../../lib/web/qsc_wasm.js";
5import type { CancellationToken } from "../cancellation.js";
6import { QdkDiagnostics } from "../diagnostics.js";
7import { log } from "../log.js";
8import type {
9 CommonEventMessage,
10 EventMessage,
11 IServiceEventMessage,
12 IServiceEventTarget,
13 IServiceProxy,
14 MethodMap,
15 RequestMessage,
16 ResponseMessage,
17 ServiceMethods,
18 ServiceProtocol,
19 ServiceState,
20} from "./types.js";
21
22export const isBrowser = typeof Worker !== "undefined";
23
24if (!isBrowser) {
25 // In CJS (esbuild bundle), require is available directly.
26 // In ESM (e.g. node --test), we use dynamic import.
27 if (typeof require === "function") {
28 // eslint-disable-next-line @typescript-eslint/no-require-imports
29 globalThis.Worker = require("web-worker");
30 } else {
31 // Dynamic import for ESM - this is lazy, Worker will be available
32 // by the time it's actually needed.
33 import("web-worker").then((mod) => {
34 globalThis.Worker = mod.default;
35 });
36 }
37 log.debug(
38 "Running in Node.js environment, using web-worker package for Worker support.",
39 );
40}
41/**
42 * Creates and initializes a service in a web worker, and returns a proxy for the service
43 * to be used from the main thread.
44 *
45 * @param workerArg The service web worker or the URL of the web worker script.
46 * @param wasmModule The wasm module to initialize the service with
47 * @param serviceProtocol An object that describes the service: its constructor, methods and events
48 * @returns A proxy object that implements the service interface.
49 * This interface can now be used as if calling into the real service,
50 * and the calls will be proxied to the web worker.
51 */
52export function createProxy<
53 TService extends ServiceMethods<TService>,
54 TServiceEventMsg extends IServiceEventMessage,
55>(
56 workerArg: string | Worker,
57 wasmModule: WebAssembly.Module,
58 serviceProtocol: ServiceProtocol<TService, TServiceEventMsg>,
59): TService & IServiceProxy {
60 // Create or use the WebWorker
61 const useModuleWorker: WorkerOptions = isBrowser
62 ? { type: "classic" }
63 : { type: "module" };
64
65 const worker =
66 typeof workerArg === "string"
67 ? new Worker(workerArg, useModuleWorker)
68 : workerArg;
69
70 // Log any errors from the worker
71 worker.addEventListener("error", (ev: Event) => {
72 log.error("Worker error:", ev);
73 });
74
75 // Send it the Wasm module to instantiate
76 worker.postMessage({
77 type: "init",
78 wasmModule,
79 qscLogLevel: log.getLogLevel(),
80 });
81
82 // If you lose the 'this' binding, some environments have issues
83 const postMessage = worker.postMessage.bind(worker);
84 const onTerminate = () => worker.terminate();
85
86 // Create the proxy which will forward method calls to the worker
87 const proxy = createProxyInternal<TService, TServiceEventMsg>(
88 postMessage,
89 onTerminate,
90 serviceProtocol.methods,
91 );
92
93 // Let proxy handle response and event messages from the worker
94 worker.addEventListener("message", (ev: MessageEvent) => {
95 proxy.onMsgFromWorker(ev.data);
96 });
97 return proxy;
98}
99
100/** Holds state for a single request received by the proxy */
101type RequestState<
102 TService extends ServiceMethods<TService>,
103 TServiceEventMsg extends IServiceEventMessage,
104> = RequestMessage<TService> & {
105 resolve: (val: any) => void;
106 reject: (err: any) => void;
107 requestEventTarget?: IServiceEventTarget<TServiceEventMsg>;
108 cancellationToken?: CancellationToken;
109};
110
111/*
112The WorkerProxy works by queuing up requests to send over to the Worker, only
113ever having one in flight at a time. By queuing on the caller side, this allows
114for cancellation (it checks if a request is cancelled before sending to the worker).
115
116The queue contains an entry for each request with the data to send, the promise
117to resolve, the event handler, and the cancellation token. When a request completes
118the next one (if present) is fetched from the queue. If it is marked as cancelled,
119it is resolved immediately, else it is marked as the current request and the command
120sent to the worker. As events occurs on the current request the event handler is
121invoked. When the response is received this is used to resolve the promise and
122complete the request.
123*/
124
125/**
126 * Function to create the proxy for a type. To be used from the main thread.
127 *
128 * @param postMessage A function to post messages to the worker
129 * @param terminator A function to call to tear down the worker thread
130 * @param methods A map of method names to be proxied and some metadata @see MethodMap
131 * @returns The proxy object. The caller should then set the onMsgFromWorker
132 * property to a callback that will receive messages from the worker.
133 */
134function createProxyInternal<
135 TService extends ServiceMethods<TService>,
136 TServiceEventMsg extends IServiceEventMessage,
137>(
138 postMessage: (msg: RequestMessage<TService>) => void,
139 terminator: () => void,
140 methods: MethodMap<TService>,
141): TService &
142 IServiceProxy & {
143 onMsgFromWorker: (
144 msg: ResponseMessage<TService> | EventMessage<TServiceEventMsg>,
145 ) => void;
146 } {
147 const queue: RequestState<TService, TServiceEventMsg>[] = [];
148 const eventTarget = new EventTarget();
149 let curr: RequestState<TService, TServiceEventMsg> | undefined;
150 let state: ServiceState = "idle";
151
152 function setState(newState: ServiceState) {
153 if (state === newState) return;
154 state = newState;
155 if (proxy.onstatechange) proxy.onstatechange(state);
156 }
157
158 type ResultOf<TRespMsg> = TRespMsg extends { result: infer R } ? R : never;
159
160 function queueRequest(
161 msg: RequestMessage<TService>,
162 requestEventTarget?: IServiceEventTarget<TServiceEventMsg>,
163 cancellationToken?: CancellationToken,
164 ): Promise<ResultOf<ResponseMessage<TService>>> {
165 return new Promise((resolve, reject) => {
166 queue.push({
167 type: msg.type,
168 args: msg.args,
169 resolve,
170 reject,
171 requestEventTarget,
172 cancellationToken,
173 } as RequestState<TService, TServiceEventMsg>);
174
175 // If nothing was running when this got added, kick off processing
176 if (queue.length === 1) doNextRequest();
177 });
178 }
179
180 function doNextRequest() {
181 if (curr) return;
182
183 while ((curr = queue.shift())) {
184 if (curr.cancellationToken?.isCancellationRequested) {
185 curr.reject("cancelled");
186 continue;
187 } else {
188 break;
189 }
190 }
191 if (!curr) {
192 // Nothing else queued, signal that we're now idle and exit.
193 log.trace("Proxy: Worker queue is empty");
194 setState("idle");
195 return;
196 }
197
198 const msg = { type: curr.type, args: curr.args };
199 if (methods[curr.type] === "requestWithProgress") {
200 setState("busy");
201 }
202
203 log.trace("Proxy: Posting message to worker: %o", msg);
204 postMessage(msg);
205 }
206
207 function onMsgFromWorker(
208 msg:
209 | ResponseMessage<TService>
210 | EventMessage<TServiceEventMsg>
211 | CommonEventMessage,
212 ) {
213 if (log.getLogLevel() >= 4)
214 log.trace("Proxy: Received message from worker: %s", JSON.stringify(msg));
215
216 if (msg.messageType === "common-event") {
217 const commonEvent = msg; // assignment is necessary here for TypeScript to narrow the type
218 switch (commonEvent.type) {
219 case "telemetry-event":
220 {
221 const detail = commonEvent.detail;
222 log.logTelemetry(detail);
223 }
224 break;
225 case "log":
226 {
227 const detail = commonEvent.detail;
228 log.logWithLevel(detail.level, detail.target, ...detail.data);
229 }
230 break;
231 }
232 } else if (msg.messageType === "event") {
233 const event = new Event(msg.type) as Event & TServiceEventMsg;
234 event.detail = msg.detail;
235
236 log.trace("Proxy: Posting event: %o", msg);
237 // Post to a currently attached event target if there's a "requestWithProgress"
238 // in progress
239 curr?.requestEventTarget?.dispatchEvent(event);
240 // Also post to the general event target
241 eventTarget.dispatchEvent(event);
242 } else if (msg.messageType === "response") {
243 if (!curr) {
244 log.error("Proxy: No active request when message received: %o", msg);
245 return;
246 }
247 const result = {
248 success: msg.result.success,
249 data: msg.result.success ? msg.result.result : msg.result.error,
250 };
251 if (result.success) {
252 curr.resolve(result.data);
253 curr = undefined;
254 doNextRequest();
255 } else {
256 let err = result.data;
257
258 // The error may be a serialized error object.
259 err = deserializeIfError(err);
260
261 curr.reject(err);
262 curr = undefined;
263 doNextRequest();
264 }
265 }
266 }
267
268 // Create the proxy object to be returned
269 const proxy = {} as TService &
270 IServiceProxy & { onMsgFromWorker: typeof onMsgFromWorker };
271
272 // Assign each method with the desired proxying behavior
273 for (const methodName of Object.keys(methods) as (keyof TService &
274 string)[]) {
275 // @ts-expect-error - tricky to derive the type of the actual method here
276 proxy[methodName] = (...args: any[]) => {
277 let requestEventTarget:
278 | IServiceEventTarget<TServiceEventMsg>
279 | undefined = undefined;
280
281 switch (methods[methodName]) {
282 case "addEventListener":
283 {
284 // @ts-expect-error - can't get the typing of the rest parameters quite right
285 eventTarget.addEventListener(...args);
286 }
287 break;
288 case "removeEventListener":
289 {
290 // @ts-expect-error - can't get the typing of the rest parameters quite right
291 eventTarget.removeEventListener(...args);
292 }
293 break;
294 case "requestWithProgress": {
295 // For progress methods, the last argument is the event target
296 requestEventTarget = args[args.length - 1];
297 args = args.slice(0, args.length - 1);
298 }
299 // fallthrough
300 case "request": {
301 return queueRequest(
302 { type: methodName, args } as RequestMessage<TService>,
303 requestEventTarget,
304 );
305 }
306 }
307 };
308 }
309
310 proxy.onstatechange = null;
311 proxy.terminate = () => {
312 // Kill the worker without a chance to shutdown. May be needed if it is not responding.
313 log.info("Proxy: Terminating the worker");
314 if (curr) {
315 log.trace(
316 "Proxy: Terminating running worker item of type: %s",
317 curr.type,
318 );
319 curr.reject("terminated");
320 }
321 // Reject any outstanding items
322 while (queue.length) {
323 const item = queue.shift();
324 log.trace(
325 "Proxy: Terminating outstanding work item of type: %s",
326 item?.type,
327 );
328 item?.reject("terminated");
329 }
330 terminator();
331 };
332 proxy.onMsgFromWorker = onMsgFromWorker;
333
334 return proxy;
335}
336
337/**
338 * Deserializes an error if it is a known type.
339 *
340 * By default, browsers can only send certain types of errors between the main thread and a worker.
341 * See: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm#error_types
342 *
343 * Serializing our own custom errors allows us to send them between threads.
344 */
345function deserializeIfError(err: unknown) {
346 if (err !== null && typeof err === "object" && "name" in err) {
347 if (err.name === "QdkDiagnostics" && "data" in err) {
348 err = new QdkDiagnostics(err.data as IQSharpError[]);
349 } else if (
350 err.name === "WebAssembly.RuntimeError" &&
351 "message" in err &&
352 (typeof err.message === "string" || typeof err.message === "undefined") &&
353 "stack" in err &&
354 (typeof err.stack === "string" || typeof err.stack === "undefined")
355 ) {
356 const newErr = new WebAssembly.RuntimeError(err.message);
357 newErr.stack = err.stack;
358 err = newErr;
359 }
360 }
361 return err;
362}
363