microsoft/qdk

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
copilot/fix-circuit-panel-error-message

Branches

Tags

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

Clone

HTTPS

Download ZIP

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

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