microsoft/qdk

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v1.21.0

Branches

Tags

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

Clone

HTTPS

Download ZIP

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

572lines · modeblame

39188fd4Mine Starks3 years ago1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
9c56278dMine Starks1 years ago4import { type IQSharpError } from "../../lib/web/qsc_wasm.js";
c23c2ae4Mine Starks2 years ago5import { CancellationToken } from "../cancellation.js";
9c56278dMine Starks1 years ago6import { QdkDiagnostics } from "../diagnostics.js";
c23c2ae4Mine Starks2 years ago7import { TelemetryEvent, log } from "../log.js";
021250d6Bill Ticehurst2 years ago8type Wasm = typeof import("../../lib/web/qsc_wasm.js");
c23c2ae4Mine Starks2 years ago9
10/**
11* Describes a service that can be run in a worker.
12*/
13export interface ServiceProtocol<
14TService extends ServiceMethods<TService>,
15TServiceEventMsg extends IServiceEventMessage,
16> {
17/** The concrete class that implements the service. */
18class: { new (wasmModule: Wasm): TService };
19/** Methods that can be proxied from the main thread to the worker. @see MethodMap*/
20methods: MethodMap<TService>;
21/** Events that can be received by the main thread from the worker. */
22eventNames: TServiceEventMsg["type"][];
23}
39188fd4Mine Starks3 years ago24
25/**
26* Used as a type constraint for a "service", i.e. an object
27* we can create proxy methods for. The type shouldn't define
28* any non-method properties.
29*/
c23c2ae4Mine Starks2 years ago30export type ServiceMethods<T> = { [x in keyof T]: (...args: any[]) => any };
39188fd4Mine Starks3 years ago31
32/**
33* Defines the service methods that the proxy will handle and their types.
c23c2ae4Mine Starks2 years ago34*
39188fd4Mine Starks3 years ago35* "request" is a normal async method.
c23c2ae4Mine Starks2 years ago36*
37* "requestWithProgress" methods take an `IServiceEventTarget` to
39188fd4Mine Starks3 years ago38* communicate events back to the main thread as they run. They also set
39* the service state to "busy" while they run.
c23c2ae4Mine Starks2 years ago40*
39188fd4Mine Starks3 years ago41* "addEventListener" and "removeEventListener" methods are used to
42* subscribe to events from the service.
43*/
44export type MethodMap<T> = {
45[M in keyof T]:
46| "request"
47| "requestWithProgress"
48| "addEventListener"
49| "removeEventListener";
50};
51
52/** Methods added to the service when wrapped in a proxy */
53export type IServiceProxy = {
54onstatechange: ((state: ServiceState) => void) | null;
55terminate: () => void;
56};
57
58/** "requestWithProgress" type methods will set the service state to "busy" */
59export type ServiceState = "idle" | "busy";
60
61/** Request message from a main thread to the worker */
62export type RequestMessage<T extends ServiceMethods<T>> = {
63[K in keyof T]: { type: K; args: Parameters<T[K]> };
64}[keyof T];
65
66/** Response message for a request from the worker to the main thread */
67export type ResponseMessage<T extends ServiceMethods<T>> = {
68messageType: "response";
69} & {
70[K in keyof T]: {
71type: K;
72result:
73| { success: true; result: Awaited<ReturnType<T[K]>> }
74| { success: false; error: unknown };
75};
76}[keyof T];
77
78/** Event message from the worker to the main thread */
79export type EventMessage<TEventMsg extends IServiceEventMessage> = {
80messageType: "event";
81} & TEventMsg;
82
83/** Used as a constraint for events defined by the service */
c23c2ae4Mine Starks2 years ago84export interface IServiceEventMessage {
39188fd4Mine Starks3 years ago85type: string;
86detail: unknown;
87}
88
ce8e749cMine Starks2 years ago89/**
90* Common event types all workers can send.
91*/
92type CommonEvent =
93| { type: "telemetry-event"; detail: TelemetryEvent }
94| {
95type: "log";
96detail: { level: number; target: string; data: any[] };
97};
98type CommonEventMessage = CommonEvent & { messageType: "common-event" };
99
39188fd4Mine Starks3 years ago100/**
101* Strongly typed EventTarget interface. Used as a constraint for the
102* event target that "requestWithProgress" methods should take in the service.
103*/
104export interface IServiceEventTarget<TEvents extends IServiceEventMessage> {
105addEventListener<T extends TEvents["type"]>(
106type: T,
b43f3f45Mine Starks2 years ago107listener: (event: Event & Extract<TEvents, { type: T }>) => void,
39188fd4Mine Starks3 years ago108): void;
109
110removeEventListener<T extends TEvents["type"]>(
111type: T,
b43f3f45Mine Starks2 years ago112listener: (event: Event & Extract<TEvents, { type: T }>) => void,
39188fd4Mine Starks3 years ago113): void;
114
115dispatchEvent(event: Event & TEvents): boolean;
116}
117
118/** Holds state for a single request received by the proxy */
119type RequestState<
120TService extends ServiceMethods<TService>,
b43f3f45Mine Starks2 years ago121TServiceEventMsg extends IServiceEventMessage,
39188fd4Mine Starks3 years ago122> = RequestMessage<TService> & {
123resolve: (val: any) => void;
124reject: (err: any) => void;
125requestEventTarget?: IServiceEventTarget<TServiceEventMsg>;
126cancellationToken?: CancellationToken;
127};
128
129/*
130The WorkerProxy works by queuing up requests to send over to the Worker, only
131ever having one in flight at a time. By queuing on the caller side, this allows
132for cancellation (it checks if a request is cancelled before sending to the worker).
133
134The queue contains an entry for each request with the data to send, the promise
135to resolve, the event handler, and the cancellation token. When a request completes
136the next one (if present) is fetched from the queue. If it is marked as cancelled,
137it is resolved immediately, else it is marked as the current request and the command
138sent to the worker. As events occurs on the current request the event handler is
139invoked. When the response is received this is used to resolve the promise and
140complete the request.
141*/
142
143/**
144* Function to create the proxy for a type. To be used from the main thread.
145*
146* @param postMessage A function to post messages to the worker
147* @param terminator A function to call to tear down the worker thread
148* @param methods A map of method names to be proxied and some metadata @see MethodMap
149* @returns The proxy object. The caller should then set the onMsgFromWorker
150* property to a callback that will receive messages from the worker.
151*/
c23c2ae4Mine Starks2 years ago152export function createProxyInternal<
39188fd4Mine Starks3 years ago153TService extends ServiceMethods<TService>,
b43f3f45Mine Starks2 years ago154TServiceEventMsg extends IServiceEventMessage,
39188fd4Mine Starks3 years ago155>(
156postMessage: (msg: RequestMessage<TService>) => void,
157terminator: () => void,
b43f3f45Mine Starks2 years ago158methods: MethodMap<TService>,
39188fd4Mine Starks3 years ago159): TService &
160IServiceProxy & {
161onMsgFromWorker: (
b43f3f45Mine Starks2 years ago162msg: ResponseMessage<TService> | EventMessage<TServiceEventMsg>,
39188fd4Mine Starks3 years ago163) => void;
164} {
165const queue: RequestState<TService, TServiceEventMsg>[] = [];
166const eventTarget = new EventTarget();
167let curr: RequestState<TService, TServiceEventMsg> | undefined;
168let state: ServiceState = "idle";
169
170function setState(newState: ServiceState) {
171if (state === newState) return;
172state = newState;
173if (proxy.onstatechange) proxy.onstatechange(state);
174}
175
176type ResultOf<TRespMsg> = TRespMsg extends { result: infer R } ? R : never;
177
178function queueRequest(
179msg: RequestMessage<TService>,
180requestEventTarget?: IServiceEventTarget<TServiceEventMsg>,
b43f3f45Mine Starks2 years ago181cancellationToken?: CancellationToken,
39188fd4Mine Starks3 years ago182): Promise<ResultOf<ResponseMessage<TService>>> {
183return new Promise((resolve, reject) => {
184queue.push({
185type: msg.type,
186args: msg.args,
187resolve,
188reject,
189requestEventTarget,
190cancellationToken,
191} as RequestState<TService, TServiceEventMsg>);
192
193// If nothing was running when this got added, kick off processing
194if (queue.length === 1) doNextRequest();
195});
196}
197
198function doNextRequest() {
199if (curr) return;
200
201while ((curr = queue.shift())) {
202if (curr.cancellationToken?.isCancellationRequested) {
203curr.reject("cancelled");
204continue;
205} else {
206break;
207}
208}
209if (!curr) {
210// Nothing else queued, signal that we're now idle and exit.
11426a16Mine Starks1 years ago211log.trace("Proxy: Worker queue is empty");
39188fd4Mine Starks3 years ago212setState("idle");
213return;
214}
215
216const msg = { type: curr.type, args: curr.args };
217if (methods[curr.type] === "requestWithProgress") {
218setState("busy");
219}
220
11426a16Mine Starks1 years ago221log.trace("Proxy: Posting message to worker: %o", msg);
39188fd4Mine Starks3 years ago222postMessage(msg);
223}
224
225function onMsgFromWorker(
ce8e749cMine Starks2 years ago226msg:
227| ResponseMessage<TService>
228| EventMessage<TServiceEventMsg>
229| CommonEventMessage,
39188fd4Mine Starks3 years ago230) {
231if (log.getLogLevel() >= 4)
11426a16Mine Starks1 years ago232log.trace("Proxy: Received message from worker: %s", JSON.stringify(msg));
39188fd4Mine Starks3 years ago233
ce8e749cMine Starks2 years ago234if (msg.messageType === "common-event") {
235const commonEvent = msg; // assignment is necessary here for TypeScript to narrow the type
236switch (commonEvent.type) {
237case "telemetry-event":
238{
239const detail = commonEvent.detail;
240log.logTelemetry(detail);
241}
242break;
243case "log":
244{
245const detail = commonEvent.detail;
246log.logWithLevel(detail.level, detail.target, ...detail.data);
247}
248break;
458eb213Bill Ticehurst3 years ago249}
ce8e749cMine Starks2 years ago250} else if (msg.messageType === "event") {
39188fd4Mine Starks3 years ago251const event = new Event(msg.type) as Event & TServiceEventMsg;
252event.detail = msg.detail;
253
11426a16Mine Starks1 years ago254log.trace("Proxy: Posting event: %o", msg);
39188fd4Mine Starks3 years ago255// Post to a currently attached event target if there's a "requestWithProgress"
256// in progress
257curr?.requestEventTarget?.dispatchEvent(event);
258// Also post to the general event target
259eventTarget.dispatchEvent(event);
260} else if (msg.messageType === "response") {
261if (!curr) {
262log.error("Proxy: No active request when message received: %o", msg);
263return;
264}
265const result = {
266success: msg.result.success,
267data: msg.result.success ? msg.result.result : msg.result.error,
268};
269if (result.success) {
270curr.resolve(result.data);
271curr = undefined;
272doNextRequest();
273} else {
9c56278dMine Starks1 years ago274let err = result.data;
275
276// The error may be a serialized error object.
277err = deserializeIfError(err);
278
279curr.reject(err);
39188fd4Mine Starks3 years ago280curr = undefined;
281doNextRequest();
282}
283}
284}
285
286// Create the proxy object to be returned
287const proxy = {} as TService &
288IServiceProxy & { onMsgFromWorker: typeof onMsgFromWorker };
289
290// Assign each method with the desired proxying behavior
291for (const methodName of Object.keys(methods) as (keyof TService &
292string)[]) {
293// @ts-expect-error - tricky to derive the type of the actual method here
294proxy[methodName] = (...args: any[]) => {
295let requestEventTarget:
296| IServiceEventTarget<TServiceEventMsg>
297| undefined = undefined;
298
299switch (methods[methodName]) {
300case "addEventListener":
301{
302// @ts-expect-error - can't get the typing of the rest parameters quite right
303eventTarget.addEventListener(...args);
304}
305break;
306case "removeEventListener":
307{
308// @ts-expect-error - can't get the typing of the rest parameters quite right
309eventTarget.removeEventListener(...args);
310}
311break;
312case "requestWithProgress": {
313// For progress methods, the last argument is the event target
314requestEventTarget = args[args.length - 1];
315args = args.slice(0, args.length - 1);
316}
317// fallthrough
318case "request": {
319return queueRequest(
320{ type: methodName, args } as RequestMessage<TService>,
b43f3f45Mine Starks2 years ago321requestEventTarget,
39188fd4Mine Starks3 years ago322);
323}
324}
325};
326}
327
328proxy.onstatechange = null;
329proxy.terminate = () => {
330// Kill the worker without a chance to shutdown. May be needed if it is not responding.
331log.info("Proxy: Terminating the worker");
332if (curr) {
11426a16Mine Starks1 years ago333log.trace(
39188fd4Mine Starks3 years ago334"Proxy: Terminating running worker item of type: %s",
b43f3f45Mine Starks2 years ago335curr.type,
39188fd4Mine Starks3 years ago336);
337curr.reject("terminated");
338}
339// Reject any outstanding items
340while (queue.length) {
341const item = queue.shift();
11426a16Mine Starks1 years ago342log.trace(
39188fd4Mine Starks3 years ago343"Proxy: Terminating outstanding work item of type: %s",
b43f3f45Mine Starks2 years ago344item?.type,
39188fd4Mine Starks3 years ago345);
346item?.reject("terminated");
347}
348terminator();
349};
350proxy.onMsgFromWorker = onMsgFromWorker;
351
352return proxy;
353}
354
355/**
356* Function to wrap a service in a dispatcher. To be used in the worker thread.
357*
358* @param service The service to be wrapped
c23c2ae4Mine Starks2 years ago359* @param methods A map of method names. Should match the list passed into @see createProxyInternal.
39188fd4Mine Starks3 years ago360* @param eventNames The list of event names that the service can emit
361* @param postMessage A function to post messages back to the main thread
362* @returns A function that takes a message and invokes the corresponding
363* method on the service. The caller should then set this method as a message handler.
364*/
c23c2ae4Mine Starks2 years ago365function createDispatcher<
39188fd4Mine Starks3 years ago366TService extends ServiceMethods<TService>,
b43f3f45Mine Starks2 years ago367TServiceEventMsg extends IServiceEventMessage,
39188fd4Mine Starks3 years ago368>(
369postMessage: (
b43f3f45Mine Starks2 years ago370msg: ResponseMessage<TService> | EventMessage<TServiceEventMsg>,
39188fd4Mine Starks3 years ago371) => void,
372service: TService,
373methods: MethodMap<TService>,
b43f3f45Mine Starks2 years ago374eventNames: TServiceEventMsg["type"][],
c23c2ae4Mine Starks2 years ago375): (req: RequestMessage<TService>) => Promise<void> {
11426a16Mine Starks1 years ago376log.trace("Worker: Constructing WorkerEventHandler");
39188fd4Mine Starks3 years ago377
378function logAndPost(
b43f3f45Mine Starks2 years ago379msg: ResponseMessage<TService> | EventMessage<TServiceEventMsg>,
39188fd4Mine Starks3 years ago380) {
11426a16Mine Starks1 years ago381log.trace(
39188fd4Mine Starks3 years ago382"Worker: Sending %s message from worker: %o",
383msg.messageType,
b43f3f45Mine Starks2 years ago384msg,
39188fd4Mine Starks3 years ago385);
386postMessage(msg);
387}
388
389const eventTarget =
390new EventTarget() as IServiceEventTarget<TServiceEventMsg>;
391
392eventNames.forEach((eventName: TServiceEventMsg["type"]) => {
393// Subscribe to all known events and forward them as messages to the main thread.
394eventTarget.addEventListener(eventName, (ev) => {
395logAndPost({
396messageType: "event",
397type: ev.type,
398detail: ev.detail,
399});
400});
401
402// If there's an addEventListener on the object itself, forward those events as well.
403if ((service as any).addEventListener) {
404(service as any).addEventListener(eventName, (ev: any) => {
405logAndPost({
406messageType: "event",
407type: ev.type,
408detail: ev.detail,
409});
410});
411}
412});
413
414return function invokeMethod(req: RequestMessage<TService>) {
415// Pass the eventTarget to the methods marked as taking progress
416return service[req.type]
417.call(
418service,
419...req.args,
b43f3f45Mine Starks2 years ago420methods[req.type] === "requestWithProgress" ? eventTarget : undefined,
39188fd4Mine Starks3 years ago421)
422.then((result: any) =>
423logAndPost({
424messageType: "response",
425type: req.type,
426result: { success: true, result },
b43f3f45Mine Starks2 years ago427}),
39188fd4Mine Starks3 years ago428)
9c56278dMine Starks1 years ago429.catch((err: any) => {
430// Serialize the error if it's a known type.
431err = serializeIfError(err);
432
39188fd4Mine Starks3 years ago433logAndPost({
434// If this happens then the wasm code likely threw an exception/panicked rather than
435// completing gracefully and fullfilling the promise. Communicate to the client
436// that there was an error and it should reject the current request
437messageType: "response",
438type: req.type,
439result: { success: false, error: err },
9c56278dMine Starks1 years ago440});
441});
39188fd4Mine Starks3 years ago442};
443}
c23c2ae4Mine Starks2 years ago444
445/**
446* Creates and initializes the actual service. To be used in the worker thread.
447*
448* @param postMessage A function to post messages back to the main thread
449* @param serviceProtocol An object that describes the service: its constructor, methods and events
450* @param wasm The wasm module to initialize the service with
451* @param qscLogLevel The log level to initialize the service with
452* @returns A function that takes a message and invokes the corresponding
453* method on the service. The caller should then set this method as a message handler.
454*/
455export function initService<
456TService extends ServiceMethods<TService>,
457TServiceEventMsg extends IServiceEventMessage,
458>(
459postMessage: (
ce8e749cMine Starks2 years ago460msg:
461| ResponseMessage<TService>
462| EventMessage<TServiceEventMsg>
463| CommonEventMessage,
c23c2ae4Mine Starks2 years ago464) => void,
465serviceProtocol: ServiceProtocol<TService, TServiceEventMsg>,
466wasm: Wasm,
467qscLogLevel?: number,
468): (req: RequestMessage<TService>) => Promise<void> {
ce8e749cMine Starks2 years ago469function postTelemetryMessage(telemetry: TelemetryEvent) {
c23c2ae4Mine Starks2 years ago470postMessage({
ce8e749cMine Starks2 years ago471messageType: "common-event",
c23c2ae4Mine Starks2 years ago472type: "telemetry-event",
473detail: telemetry,
474});
475}
476
ce8e749cMine Starks2 years ago477function postLogMessage(level: number, target: string, ...args: any) {
478if (log.getLogLevel() < level) {
479return;
480}
481
482let data = args;
483try {
484// Only structured cloneable objects can be sent in worker messages.
485// Test if this is the case.
486structuredClone(args);
4d6e26abBill Ticehurst2 years ago487} catch {
ce8e749cMine Starks2 years ago488// Uncloneable object.
489// Use String(args) instead of ${args} to handle all possible values
490// without throwing. See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String#string_coercion
491data = ["unsupported log data " + String(args)];
492}
493postMessage({
494messageType: "common-event",
495type: "log",
496detail: { level, target, data },
497});
498}
499
500// Override the global logger
501log.error = (...args) => postLogMessage(1, "worker", ...args);
502log.warn = (...args) => postLogMessage(2, "worker", ...args);
503log.info = (...args) => postLogMessage(3, "worker", ...args);
504log.debug = (...args) => postLogMessage(4, "worker", ...args);
505log.trace = (...args) => postLogMessage(5, "worker", ...args);
506
c23c2ae4Mine Starks2 years ago507if (qscLogLevel !== undefined) {
508log.setLogLevel(qscLogLevel);
509}
510
511// Set up logging and telemetry as soon as possible after instantiating
512log.onLevelChanged = (level) => wasm.setLogLevel(level);
ce8e749cMine Starks2 years ago513log.setTelemetryCollector(postTelemetryMessage);
514wasm.initLogging(postLogMessage, log.getLogLevel());
c23c2ae4Mine Starks2 years ago515
516// Create the actual service and return the dispatcher method
517const service = new serviceProtocol.class(wasm);
518return createDispatcher(
519postMessage,
520service,
521serviceProtocol.methods,
522serviceProtocol.eventNames,
523);
524}
9c56278dMine Starks1 years ago525
526/**
527* Serializes an error, if it is a known type, so that it can be sent between threads.
528*
529* By default, browsers can only send certain types of errors between the main thread and a worker.
530* See: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm#error_types
531*
532* Serializing our own custom errors allows us to send them between threads.
533*/
534function serializeIfError(err: unknown) {
535if (err instanceof QdkDiagnostics) {
536err = { name: err.name, data: err.diagnostics };
89832b9fMine Starks1 years ago537} else if (err instanceof WebAssembly.RuntimeError) {
538err = {
539name: "WebAssembly.RuntimeError",
540message: err.message,
541stack: err.stack,
542};
9c56278dMine Starks1 years ago543}
544return err;
545}
546
547/**
548* Deserializes an error if it is a known type.
549*
550* By default, browsers can only send certain types of errors between the main thread and a worker.
551* See: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm#error_types
552*
553* Serializing our own custom errors allows us to send them between threads.
554*/
555function deserializeIfError(err: unknown) {
89832b9fMine Starks1 years ago556if (err !== null && typeof err === "object" && "name" in err) {
557if (err.name === "QdkDiagnostics" && "data" in err) {
558err = new QdkDiagnostics(err.data as IQSharpError[]);
559} else if (
560err.name === "WebAssembly.RuntimeError" &&
561"message" in err &&
562(typeof err.message === "string" || typeof err.message === "undefined") &&
563"stack" in err &&
564(typeof err.stack === "string" || typeof err.stack === "undefined")
565) {
566const newErr = new WebAssembly.RuntimeError(err.message);
567newErr.stack = err.stack;
568err = newErr;
569}
9c56278dMine Starks1 years ago570}
571return err;
572}