mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-25 08:38:12 +08:00
Remove editor instances concept and clean up JS interop code
This commit is contained in:
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user