// Our messages: // e.data should contain a "message" field with the method name, // a "params" field with the method arguments, and an optional // "id" field for the response. // Emscripten inter-thread communication messages: // e.data contains a "cmd" field to signal what to do to the worker. // In order to make threading work, we have to go through a whole bunch of setup. // We're using PROXY_TO_PTHREAD emscripten option, which allows our code to create // pthreads synchronously and join them immediately. Without that, we'd have a predefined // pool of workers and trying to create more pthreads than unused workers would silently deadlock. // PROXY_TO_PTHREAD works by having the main worker spawn a pthread worker // -- that we'll call "proxied worker/thread" -- // that runs main(). When proxied thread calls pthread_create(), it sends a message // to main worker that creates a new worker to host the pthread (or reuses an existing one). // Creating a new worker requires yielding to the browser event loop, that's why // we can't just create workers in the proxied thread. Proxied thread can wait // on pthread_create() synchronously while main worker yields. // Emscripten assumes we run directly in the browser (not in a worker) so it would create // a worker to run main() and proxy rendering and event handling to main browser thread. // Our usage is very different - we're running in a worker (so we end up two layers deep) // and we want to respond to outside events instead of having all app logic in a main() function. /* 1. First the zenid-web-sdk.js loads this file to a worker. We install a handler to listen to messages (see bottom of this file). 3. We import the worker script generated by emscripten at zenid-wasm-mt.js, on the next browser tick it will start loading the wasm file, instantiate it and eventually run callMain() which will create a pthread and run our actual main() in it. 4. But before that happens we replace PThread.getNewWorker() by our own for a single call. It will get called as part of creating the proxied pthread. Normally getNewWorker() loads zenid-wasm-mt.js but we want to load zenid-worker.js again. This instance will be our actual one, and the parent instance will become a transparent proxy for messages. We save the proxied worker handle to self.proxiedWorker (we'll need it later) and hijack the onmessage handler with our own that transparently proxies our messages out of the worker and sends emscripten messages to the original handler. Then we replace our worker's onmessage with one that transparently proxies our messages from outside to the proxied worker and sends the rest to the original handler. At this point our worker has become a transparent proxy for our messages while still handling emscripten messages. Now let's see what's happening in the proxied worker: 5. We import emscripten worker code again (see bottom of this file). Since we set the name of this worker to em-pthread, emscripten code assumes we're in a newly created pthread (it checks self.name for that), so it does all the threading setup, installs event handlers and eventually runs main() as though we loaded zenid-wasm-mt.js instead of zenid-worker.js 6. main() calls runOnChallenge() js function from C++. Running that function is a signal for us that the proxied thread has initialized and ran main(), so we're ready to do our own setup. We can't run it in onRuntimeInitialized or postRun events, because proxying machinery would not be in place yet (?). Now we install our own event handler that will route our messages to the right methods and route emscripten messages to its own handler so that pthread interop keeps working. Then we send an 'onChallenge' message to tell zenid-web-sdk.js that we're ready to start authorization process. 7. The 'onChallenge' message is proxied to zenid-web-sdk.js by the parent worker and authorization and loading process continues as normal, with messages transparently proxied back and forth. */ // We want to import worker-interfaces.ts // But can't use an import statement in a non-module worker. // And if we switch to a module worker we can no longer use importScripts(). // And if we try importScripts("./worker-interfaces.js") it will complain about export keyword. // And if we remove export keyword, we can't import it in zenid-sdk.ts // So the only solution it straight-up copy it here: // IMPORTED FROM WORKER-INTERFACES enum ActionType { Init = 'init', // Start the worker initialization Call = 'call', // Call any exported WASM function universally Terminate = 'terminate', // Terminate the worker } // --- Incoming Message Interfaces --- class WorkerInitAction { readonly action = ActionType.Init; } // --- Incoming Message Interfaces --- class WorkerTerminateAction { readonly action = ActionType.Terminate; } class WorkerCallAction { readonly action = ActionType.Call; constructor( public id: number | string, public functionName: string, public params: any[], public transferables?: ArrayBuffer[] ) {} } // Union type for incoming actions type WorkerIncomingAction = WorkerInitAction | WorkerCallAction | WorkerTerminateAction; // --- Outgoing Message Interfaces --- enum WorkerResponseType { Result = 'result', // Result of a successful 'Call' Error = 'error', // Error occurred during initialization or processing Initialized = 'initialized', // Worker setup complete, WASM runtime ready in proxied thread Log = 'log', // For sending log messages } interface WorkerResponse { type: WorkerResponseType; id?: number | string; // For call results or errors result?: any; // For successful call results logMessage?: string; // For log messages logLevel?: number; // For log messages functionName?: string; // For error messages error?: string; // For error messages } // END IMPORTS FROM WORKER-INTERFACES // --- Configuration --- const relativePath: string = "./"; // --- Globals const workerScriptUrl: string = self.location.href; const moduleBaseUrl: string = new URL(relativePath, workerScriptUrl).href; // SDK side passes absolute wasm + emscripten-glue URLs as query params on the worker URL // (see zenid-sdk.ts setup()). When this worker spawns the proxied em-pthread child via // `new Worker(self.location.href, ...)`, those params propagate automatically. Falling // back to relative resolution keeps direct importmap usage (no bundler) working. const wasmParams: URLSearchParams = new URLSearchParams(self.location.search); const wasmFilePath: string = wasmParams.get('wasm') || (moduleBaseUrl + "zenid-wasm-mt.wasm"); const jsFilePath: string = wasmParams.get('js') || (moduleBaseUrl + "zenid-wasm-mt.js"); // Emscripten asks locateFile() for "zenid-wasm-mt.wasm" — redirect to the absolute URL, // otherwise it would fall through to scriptDirectory + path which is wrong under bundling. const locateWasm = (file: string) => (file.endsWith('.wasm') ? wasmFilePath : file); // this object is used by Emscripten to fill in native methods and for various callbacks let ZenidModule: any = { locateFile: locateWasm }; // forward declarations declare var PThread: any; // Emscripten PThread object (for proxying) // --- State Management (Internal Worker States) --- enum WasmState { uninitialized = 0, // Before Init message received initializing = 1, // Main worker setting up proxy & proxied thread initializing runtime initialized = 2 // Proxied thread's WASM runtime ready, final message handler active } let wasmState: WasmState = WasmState.uninitialized; // State tracked within the current worker context let proxiedThread: Worker | null = null; // Handle to the proxied worker // Helper function to post messages with empty transfer list function postMessageToMain(message: any): void { self.postMessage(message, []); } function postLog(message: string, level: number = 2) { const logResponse: WorkerResponse = { type: WorkerResponseType.Log, logMessage: `[Worker] ${message}`, logLevel: level }; postMessageToMain(logResponse); } function postError(message: string, id: string | number = 'general_error', functionName: string = 'unknown') { postLog(`(ID: ${String(id)}, Func: ${functionName}): ${message}`); const errorResponse: WorkerResponse = { type: WorkerResponseType.Error, id: id, functionName, error: message }; postMessageToMain(errorResponse); } function safeCall(func: () => T): T { try { return func(); } catch (error: any) { let errorMessage: string; if (error instanceof Error) { errorMessage = error.message; } else if (typeof error === 'string') { errorMessage = error; } else { errorMessage = ZenidModule?.nativeGetExceptionMessage?.(error) ?? String(error); } postLog(`Error during safeCall: ${errorMessage} ${error}`, 0); throw new Error(errorMessage); } } // --- WASM Interaction Logic (Assumes running in proxied thread) --- function preprocessArgs(args: any[], allocatedPtrs: number[]): any[] { if (typeof ZenidModule?.['nativeCreateImage'] !== 'function' || !ZenidModule?.HEAP8) { return args; } const processedArgs: any[] = []; let i = 0; while (i < args.length) { const current = args[i]; const next1 = args[i + 1]; const next2 = args[i + 2]; if (i + 2 < args.length && (current instanceof ArrayBuffer || ArrayBuffer.isView(current)) && typeof next1 === 'number' && typeof next2 === 'number') { const imageData: ArrayBuffer | ArrayBufferView = current; const width: number = next1; const height: number = next2; const imagePtr = safeCall(() => ZenidModule['nativeCreateImage'](width, height)); if (typeof imagePtr !== 'number' || imagePtr === 0) { throw new Error(`nativeCreateImage(${width}, ${height}) failed.`); } allocatedPtrs.push(imagePtr); const uint8Data = imageData instanceof ArrayBuffer ? new Uint8Array(imageData) : new Uint8Array(imageData.buffer, imageData.byteOffset, imageData.byteLength); safeCall(() => ZenidModule.HEAP8.set(uint8Data, imagePtr)); processedArgs.push(imagePtr); // push next1 and next2 to processedArgs processedArgs.push(width); processedArgs.push(height); i += 3; // Skip the next two arguments } else { processedArgs.push(current); i += 1; } } return processedArgs; } function cleanupAllocatedMemory(allocatedPtrs: number[]) { if (allocatedPtrs.length === 0) return; if (typeof ZenidModule?.['nativeDestroyImage'] === 'function') { for (const ptr of allocatedPtrs) { try { ZenidModule['nativeDestroyImage'](ptr); } catch (e: any) { postError(`Error destroying image pointer ${ptr}: ${e.message}`, 'cleanup_error', 'nativeDestroyImage'); } } } else { postLog(`Allocated pointers exist, but nativeDestroyImage is not available.`, 1); } } function callNativeFunction(functionName: string, args: any[]): any { if (!ZenidModule || typeof ZenidModule[functionName] !== 'function') { throw new Error(`Function ${functionName} not found.`); } if (wasmState !== WasmState.initialized) { throw new Error(`WASM module not initialized.`); } const allocatedPtrs: number[] = []; try { const processedArgs = preprocessArgs(args, allocatedPtrs); return safeCall(() => ZenidModule[functionName].apply(ZenidModule, processedArgs)); } finally { cleanupAllocatedMemory(allocatedPtrs); } } // --- runOnChallenge (Called by C++ main() in proxied thread via EM_ASM) when it enters the main loop --- let originalOnMessage : (((this: WindowEventHandlers, ev: MessageEvent) => any) & ((this: Window, ev: MessageEvent) => any)) | null; // Original onmessage handler function runOnChallenge(): void { postLog("Worker (proxied): runOnChallenge hook called by WASM main()."); try { originalOnMessage = self.onmessage; self.onmessage = handleProxiedMessage; // Set final handler FIRST wasmState = WasmState.initialized; // Set state THEN postLog("Worker (proxied): Runtime initialized. Switched to final message handler."); const initResponse: WorkerResponse = { type: WorkerResponseType.Initialized }; postMessageToMain(initResponse); // Signal readiness LAST postLog("Worker (proxied): Sent Initialized signal."); } catch (error: any) { const errorMsg = error instanceof Error ? error.message : String(error); postError(`Error during runOnChallenge setup: ${errorMsg}`, 'runOnChallenge_error', 'runOnChallenge'); wasmState = WasmState.uninitialized; const errorResponse: WorkerResponse = { type: WorkerResponseType.Error, id: 'runOnChallenge_error', functionName: 'runOnChallenge', error: `Proxied thread initialization failed: ${errorMsg}` }; postMessageToMain(errorResponse); } } // --- Message Handlers --- /** Handles messages WITHIN the proxied (em-pthread) worker AFTER initialization. */ function handleProxiedMessage(e: MessageEvent): void { if (e.data.action !== ActionType.Call) { // invoke the original handler if it exists postLog(`[Worker (proxied)] Received unknown message structure or action: ${e.data}`); if (originalOnMessage) originalOnMessage.call(self, e); return; } const { id, functionName, params: args } = e.data; // Known to be WorkerCallMessage if (wasmState !== WasmState.initialized || !ZenidModule) { postError(`Proxied worker received message '${functionName}' but WASM runtime not ready.`, id, functionName); return; } try { const result = callNativeFunction(functionName, args); const resultResponse: WorkerResponse = { type: WorkerResponseType.Result, id, functionName, result }; postMessageToMain(resultResponse); } catch (error: any) { postError(error.message, id, functionName); } } /** Handles messages in the MAIN worker, proxying them TO the proxied worker */ function handleProxyMessage(e: MessageEvent): void { if (e.data && e.data.cmd) { return; } // Ignore Emscripten internal commands const messageData = e.data as WorkerIncomingAction; if (messageData?.action === ActionType.Call) { const callMessage = messageData as WorkerCallAction; if (proxiedThread) { const transferList = callMessage.transferables || []; proxiedThread.postMessage(callMessage, transferList); } else { postError("Proxied thread not available, cannot forward message.", callMessage.id, callMessage.action); } } else if (messageData?.action === ActionType.Terminate) { PThread.terminateAllThreads(); // yield to event loop out of abundance of caution setTimeout(() => self.close(), 0); } else { const id = (e.data as any)?.id ?? 'unknown_message'; const action = (e.data as any)?.action ?? 'unknown'; postError("Unknown message structure or action received by proxy handler", id, action); } } /** Handles the VERY FIRST message to the main worker to trigger initialization */ function handleInitialMessage(e: MessageEvent): void { if (e.data.action === ActionType.Init) { if (wasmState === WasmState.uninitialized) { postLog("[Main Worker] Received Init message. Starting PThread proxy setup..."); wasmState = WasmState.initializing; setupProxyAndLoadEmscripten().catch(error => { const errorMsg = error instanceof Error ? error.message : String(error); postError(`Initialization failed: ${errorMsg}`, 'setup_error', 'setupProxyAndLoadEmscripten'); wasmState = WasmState.uninitialized; }); } else { postLog(`[Main Worker] Received Init message but already initializing/initialized.`, 1); } } else { let id_to_report: string | number = 'unknown_action_id'; if (e.data.action === ActionType.Call) { id_to_report = (e.data as WorkerCallAction).id ?? 'malformed_call_id'; } postError(`Received message action '${e.data.action}' before worker is initialized. Send '${ActionType.Init}' first.`, id_to_report, e.data.action); } } // --- PThread Proxy Setup Function (Called by handleInitialMessage) --- async function setupProxyAndLoadEmscripten(): Promise { try { postLog(`[Main Worker] Importing Emscripten script: ${jsFilePath}`); importScripts(jsFilePath); // Load main Emscripten JS postLog(`[Main Worker] Script imported.`); if (typeof PThread === 'undefined' || !PThread || typeof PThread.getNewWorker !== 'function') { throw new Error("PThread object/getNewWorker not found. Check Emscripten flags."); } const getNewWorkerOrig = PThread.getNewWorker; postLog("[Main Worker] Hijacking PThread.getNewWorker..."); PThread.getNewWorker = () => { postLog("[Main Worker] PThread.getNewWorker hijacked call executing."); // ... (worker creation) ... const newWorker = new Worker(self.location.href, { name: 'em-pthread' }); proxiedThread = newWorker; const currentProxiedWorker = newWorker; postLog("[Main Worker] Asking Emscripten to load WASM module into proxied thread..."); // We need to attach the forwarding handler *after* Emscripten might have // done some initial setup on the worker during loadWasmModuleToWorker. // Let's attach it inside the .then() callback. PThread.loadWasmModuleToWorker(currentProxiedWorker).then(() => { postLog(`[Main Worker] Emscripten reported WASM module loaded into proxied thread.`); // *** Attach the crucial forwarding handler HERE *** const originalProxiedMessageHandler = currentProxiedWorker.onmessage; // Capture handler *after* load currentProxiedWorker.onmessage = (ev: MessageEvent) => { const msgData = ev.data; if (msgData && msgData.type && !msgData.cmd) { // It's our message (Initialized, Result, Error, Log from proxied thread) // Forward it OUTSIDE the main worker to the main application // console.log(`[Main Worker] Forwarding message OUT: ${msgData.type}`); // Debug line postMessageToMain(msgData); } else if (originalProxiedMessageHandler && msgData?.cmd) { // It's likely an internal Emscripten message for the proxied thread's state originalProxiedMessageHandler.call(currentProxiedWorker, ev); } else if (msgData?.cmd) { postLog(`[Main Worker] Received Emscripten cmd but no original handler captured. ${msgData.cmd}`); } }; postLog("[Main Worker] Attached forwarding onmessage handler to proxied thread handle."); }).catch((err: any) => { // ... (error handling remains the same) ... const errorMsg = err instanceof Error ? err.message : String(err); postError(`Failed PThread.loadWasmModuleToWorker: ${errorMsg}`, 'loadWasm_error', 'loadWasmModuleToWorker'); currentProxiedWorker?.terminate(); if(proxiedThread === currentProxiedWorker) { proxiedThread = null; } wasmState = WasmState.uninitialized; }); PThread.getNewWorker = getNewWorkerOrig; // Restore can happen earlier postLog("[Main Worker] Restored original PThread.getNewWorker."); // Subsequent pthreads (spawned by the restored getNewWorker) go through // PThread.allocateUnusedWorker, which by default does // new Worker("zenid-wasm-mt.js", { name: "em-pthread" }) // with a hardcoded RELATIVE URL. Under bundling the worker file's directory // doesn't contain the emscripten glue, so we patch it to use the absolute URL // we already received from the SDK side. PThread.allocateUnusedWorker = () => { const w = new Worker(jsFilePath, { name: "em-pthread" }); PThread.unusedWorkers.push(w); }; return currentProxiedWorker; // Return worker handle }; self.onmessage = handleProxyMessage; // Set final handler for this main worker postLog("[Main Worker] Final message handler set up."); postLog("[Main Worker] Setup complete. Waiting for Emscripten init in proxied thread."); } catch (error: any) { const errorMsg = error instanceof Error ? error.message : String(error); postError(`Initialization failed during setup: ${errorMsg}`, 'setup_error', 'setupProxyAndLoadEmscripten'); wasmState = WasmState.uninitialized; self.onmessage = handleInitialMessage; // Revert on failure } } // --- Worker Entry Point --- if (typeof self !== 'undefined' && self.name === "em-pthread") { // ========================== PROXIED THREAD ========================== wasmState = WasmState.initializing; postLog("Worker: Running as proxied em-pthread."); ZenidModule = { // Define stubs for Emscripten runtime print: (text: string) => postLog(`[WASM stdout] ${text}`), printErr: (text: string) => postLog(`[WASM stderr] ${text}`, 0), setStatus: (text: string) => postLog(`[WASM setStatus] ${text}`), onRuntimeInitialized: () => { postLog("Worker (proxied): Emscripten runtime initialized cb fired."); }, locateFile: locateWasm, }; try { postLog(`Worker (proxied): Importing Emscripten script: ${jsFilePath}`); importScripts(jsFilePath); // Load main Emscripten JS postLog("Worker (proxied): Script imported. Emscripten should now run main()."); } catch (error: any) { const errorMsg = error instanceof Error ? error.message : String(error); postError(`Error importing scripts in proxied thread: ${errorMsg}`, 'import_error', 'importScripts'); wasmState = WasmState.uninitialized; postMessageToMain({ type: WorkerResponseType.Error, id: 'import_error', functionName: 'importScripts', error: `Proxied thread script import failed: ${errorMsg}` }); } } else if (typeof self !== 'undefined') { // ========================== MAIN PROXY THREAD ========================== wasmState = WasmState.uninitialized; postLog("Worker: Running as main proxy worker. Waiting for Init message."); self.onmessage = handleInitialMessage; // Set initial handler } else { postError("Worker: Environment context (`self`) is undefined."); }