Graphite/frontend/src/state/wasm-loader.ts

90 lines
3.4 KiB
TypeScript

/* 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<WasmInstance["JsEditorHandle"]>;
// `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<void> {
// 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<T extends object>(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<T>(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<typeof createJsDispatcher>;
/// 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<string, unknown>): void => {
dispatcher.handleJsMessage(messageType, data, rawWasm, instance);
});
return {
rawWasm,
dispatcher,
instance,
};
}
export type EditorState = Readonly<ReturnType<typeof createEditorState>>;