/* eslint-disable func-names */ import { createJsDispatcher } from "@/dispatcher/js-dispatcher"; import { JsMessageType } from "@/dispatcher/js-messages"; export type WasmInstance = typeof import("@/../wasm/pkg"); export type RustEditorInstance = InstanceType; // `wasmImport` starts uninitialized until `initWasm()` is called in `main.ts` before the Vue app is created let wasmImport: WasmInstance | null = null; export async function initWasm(): Promise { // Skip if the wasm module is already initialized if (wasmImport !== null) return; // Separating in two lines satisfies typescript when used below const importedWasm = await import("@/../wasm/pkg").then(panicProxy); wasmImport = importedWasm; // Provide a random starter seed which must occur after initializing the wasm module, since wasm can't generate is own random numbers const randomSeed = BigInt(Math.floor(Math.random() * Number.MAX_SAFE_INTEGER)); importedWasm.set_random_seed(randomSeed); } // This works by proxying every function call and wrapping a try-catch block to filter out redundant and confusing // `RuntimeError: unreachable` exceptions that would normally be printed in the browser's JS console upon a panic. function panicProxy(module: T): T { const proxyHandler = { get(target: T, propKey: string | symbol, receiver: unknown): unknown { const targetValue = Reflect.get(target, propKey, receiver); // Keep the original value being accessed if it isn't a function const isFunction = typeof targetValue === "function"; if (!isFunction) return targetValue; // Special handling to wrap the return of a constructor in the proxy const isClass = isFunction && /^\s*class\s+/.test(targetValue.toString()); if (isClass) { return function (...args: unknown[]): unknown { // eslint-disable-next-line new-cap const result = new targetValue(...args); return panicProxy(result); }; } // Replace the original function with a wrapper function that runs the original in a try-catch block return function (...args: unknown[]): unknown { let result; try { // @ts-expect-error TypeScript does not know what `this` is, since it should be able to be anything result = targetValue.apply(this, args); } catch (err) { // Suppress `unreachable` WebAssembly.RuntimeError exceptions if (!`${err}`.startsWith("RuntimeError: unreachable")) throw err; } return result; }; }, }; return new Proxy(module, proxyHandler); } export function getWasmInstance(): WasmInstance { if (wasmImport) return wasmImport; throw new Error("Editor WASM backend was not initialized at application startup"); } type CreateEditorStateType = { // Allows subscribing to messages from the WASM backend rawWasm: WasmInstance; // Bindings to WASM wrapper declarations (generated by wasm-bindgen) dispatcher: ReturnType; // WASM wrapper's exported functions (generated by wasm-bindgen) instance: RustEditorInstance; }; export function createEditorState(): CreateEditorStateType { const rawWasm = getWasmInstance(); const dispatcher = createJsDispatcher(); const instance = new rawWasm.JsEditorHandle((messageType: JsMessageType, data: Record): void => { dispatcher.handleJsMessage(messageType, data, rawWasm, instance); }); return { rawWasm, dispatcher, instance, }; } export type EditorState = Readonly>;