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