microsoft/qdk

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
iadavis/spike-3310

Branches

Tags

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

Clone

HTTPS

Download ZIP

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

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