Remove editor instances concept and clean up JS interop code

This commit is contained in:
Keavon Chambers
2024-04-29 04:17:09 -07:00
parent 597c96a7db
commit 19eb6ce0ab
25 changed files with 256 additions and 325 deletions
+24 -41
View File
@@ -1,30 +1,18 @@
// import { panicProxy } from "@graphite/utility-functions/panic-proxy";
import { type JsMessageType } from "@graphite/wasm-communication/messages";
import { createSubscriptionRouter, type SubscriptionRouter } from "@graphite/wasm-communication/subscription-router";
import init, { setRandomSeed, wasmMemory, JsEditorHandle } from "@graphite-frontend/wasm/pkg/graphite_wasm.js";
import init, { setRandomSeed, wasmMemory, EditorHandle } from "@graphite-frontend/wasm/pkg/graphite_wasm.js";
export type WasmRawInstance = WebAssembly.Memory;
export type WasmEditorInstance = JsEditorHandle;
export type Editor = Readonly<ReturnType<typeof createEditor>>;
export type Editor = {
raw: WebAssembly.Memory;
handle: EditorHandle;
subscriptions: SubscriptionRouter;
};
// `wasmImport` starts uninitialized because its initialization needs to occur asynchronously, and thus needs to occur by manually calling and awaiting `initWasm()`
let wasmImport: WebAssembly.Memory | undefined;
const tauri = "__TAURI_METADATA__" in window && import("@tauri-apps/api");
export async function dispatchTauri(message: unknown) {
if (!tauri) return;
try {
const response = await (await tauri).invoke("handle_message", { message });
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(window as any).editorInstance?.tauriResponse(response);
} catch {
// eslint-disable-next-line no-console
console.error("Failed to dispatch Tauri message");
}
}
// Should be called asynchronously before `createEditor()`
// Should be called asynchronously before `createEditor()`.
export async function initWasm() {
// Skip if the WASM module is already initialized
if (wasmImport !== undefined) return;
@@ -40,27 +28,26 @@ export async function initWasm() {
const randomSeedFloat = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER);
const randomSeed = BigInt(randomSeedFloat);
setRandomSeed(randomSeed);
if (!tauri) return;
await (await tauri).invoke("set_random_seed", { seed: randomSeedFloat });
}
// Should be called after running `initWasm()` and its promise resolving
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export function createEditor() {
// Raw: Object containing several callable functions from `editor_api.rs` defined directly on the WASM module, not the editor instance (generated by wasm-bindgen)
// Should be called after running `initWasm()` and its promise resolving.
export function createEditor(): Editor {
// Raw: object containing several callable functions from `editor_api.rs` defined directly on the WASM module, not the `EditorHandle` struct (generated by wasm-bindgen)
if (!wasmImport) throw new Error("Editor WASM backend was not initialized at application startup");
const raw: WasmRawInstance = wasmImport;
const raw: WebAssembly.Memory = wasmImport;
// Instance: Object containing many functions from `editor_api.rs` that are part of the editor instance (generated by wasm-bindgen)
const instance: WasmEditorInstance = new JsEditorHandle((messageType: JsMessageType, messageData: Record<string, unknown>) => {
// This callback is called by WASM when a FrontendMessage is received from the WASM wrapper editor instance
// We pass along the first two arguments then add our own `raw` and `instance` context for the last two arguments
subscriptions.handleJsMessage(messageType, messageData, raw, instance);
// Handle: object containing many functions from `editor_api.rs` that are part of the `EditorHandle` struct (generated by wasm-bindgen)
const handle: EditorHandle = new EditorHandle((messageType: JsMessageType, messageData: Record<string, unknown>) => {
// This callback is called by WASM when a FrontendMessage is received from the WASM wrapper `EditorHandle`
// We pass along the first two arguments then add our own `raw` and `handle` context for the last two arguments
subscriptions.handleJsMessage(messageType, messageData, raw, handle);
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(window as any).editorInstance = instance;
// Subscriptions: Allows subscribing to messages in JS that are sent from the WASM backend
// TODO: Remove?
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(window as any).editorHandle = handle;
// Subscriptions: allows subscribing to messages in JS that are sent from the WASM backend
const subscriptions: SubscriptionRouter = createSubscriptionRouter();
// Check if the URL hash fragment has any demo artwork to be loaded
@@ -75,7 +62,7 @@ export function createEditor() {
const filename = url.pathname.split("/").pop() || "Untitled";
const content = await data.text();
instance.openDocumentFile(filename, content);
handle.openDocumentFile(filename, content);
// Remove the hash fragment from the URL
history.replaceState("", "", `${window.location.pathname}${window.location.search}`);
@@ -84,14 +71,10 @@ export function createEditor() {
}
})();
return {
raw,
instance,
subscriptions,
};
return { raw, handle, subscriptions };
}
export function injectImaginatePollServerStatus() {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(window as any).editorInstance?.injectImaginatePollServerStatus();
(window as any).editorHandle?.injectImaginatePollServerStatus();
}
+2 -2
View File
@@ -4,7 +4,7 @@
import { Transform, Type, plainToClass } from "class-transformer";
import { type PopoverButtonStyle, type IconName, type IconSize } from "@graphite/utility-functions/icons";
import { type WasmEditorInstance, type WasmRawInstance } from "@graphite/wasm-communication/editor";
import { type EditorHandle } from "@graphite-frontend/wasm/pkg/graphite_wasm.js";
export class JsMessage {
// The marker provides a way to check if an object is a sub-class constructor for a jsMessage.
@@ -1275,7 +1275,7 @@ function createMenuLayoutRecursive(children: any[][]): MenuBarEntry[][] {
// `any` is used since the type of the object should be known from the Rust side
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type JSMessageFactory = (data: any, wasm: WasmRawInstance, instance: WasmEditorInstance) => JsMessage;
type JSMessageFactory = (data: any, wasm: WebAssembly.Memory, handle: EditorHandle) => JsMessage;
type MessageMaker = typeof JsMessage | JSMessageFactory;
export const messageMakers: Record<string, MessageMaker> = {
@@ -1,7 +1,7 @@
import { plainToInstance } from "class-transformer";
import { type WasmEditorInstance, type WasmRawInstance } from "@graphite/wasm-communication/editor";
import { type JsMessageType, messageMakers, type JsMessage } from "@graphite/wasm-communication/messages";
import { type EditorHandle } from "@graphite-frontend/wasm/pkg/graphite_wasm.js";
type JsMessageCallback<T extends JsMessage> = (messageData: T) => void;
// Don't know a better way of typing this since it can be any subclass of JsMessage
@@ -17,7 +17,7 @@ export function createSubscriptionRouter() {
subscriptions[messageType.name] = callback;
};
const handleJsMessage = (messageType: JsMessageType, messageData: Record<string, unknown>, wasm: WasmRawInstance, instance: WasmEditorInstance) => {
const handleJsMessage = (messageType: JsMessageType, messageData: Record<string, unknown>, wasm: WebAssembly.Memory, handle: EditorHandle) => {
// Find the message maker for the message type, which can either be a JS class constructor or a function that returns an instance of the JS class
const messageMaker = messageMakers[messageType];
if (!messageMaker) {
@@ -42,7 +42,7 @@ export function createSubscriptionRouter() {
// If the `messageMaker` is a `JsMessage` class then we use the class-transformer library's `plainToInstance` function in order to convert the JSON data into the destination class.
// If it is not a `JsMessage` then it should be a custom function that creates a JsMessage from a JSON, so we call the function itself with the raw JSON as an argument.
// The resulting `message` is an instance of a class that extends `JsMessage`.
const message = messageIsClass ? plainToInstance(messageMaker, unwrappedMessageData) : messageMaker(unwrappedMessageData, wasm, instance);
const message = messageIsClass ? plainToInstance(messageMaker, unwrappedMessageData) : messageMaker(unwrappedMessageData, wasm, handle);
// If we have constructed a valid message, then we try and execute the callback that the frontend has associated with this message.
// The frontend should always have a callback for all messages, but due to message ordering, we might have to delay a few stack frames until we do.