mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-30 18:08:12 +08:00
Refactor messages.ts by removing class-transformer and JS classes (#3858)
* Fix gamma correction with HTML-based editable Text tool text * Migrate simple, undecorated classes to types * Remove TupleToVec2 transformation * Remove @Transform from tooltips * Cleanup: replace value.toString() with String(value) everywhere * Convert documentId from string to bigint * Migrate the rest of the easy @Transform/@Type decorations * Migrate FillChoice * Migrate WidgetDiffUpdate * Migrate WidgetInstance * Migrate away from classes that extend WidgetProps * Remove class-transformer and all classes in messages.ts * Migrate UI layout passing * Remove dead code * Remove unnecessary export and readonly prefixes * Remove HSVA type * Break out Color, Gradient, and FillChoice functions into a utility-functions file * Move widget helper functions from messages.ts into a new utility-functions file; restructure type imports * Reduce internal type defs * Rename JsMessage to FrontendMessage * Code review fixes * Fix other usages * Tidying up
This commit is contained in:
@@ -1,68 +1,66 @@
|
||||
import { plainToInstance } from "class-transformer";
|
||||
import type { FrontendMessages, LayoutTarget, WidgetDiff } from "@graphite/messages";
|
||||
import { parseWidgetDiffs } from "@graphite/utility-functions/widgets";
|
||||
|
||||
import { type EditorHandle } from "@graphite/../wasm/pkg/graphite_wasm";
|
||||
import { type JsMessageType, messageMakers, type JsMessage } from "@graphite/messages";
|
||||
|
||||
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
|
||||
// The functions interacting with this map are strongly typed though around JsMessage
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type JsMessageCallbackMap = Record<string, JsMessageCallback<any> | undefined>;
|
||||
type FrontendMessageCallbacks = Record<string, ((messageData: any) => void) | undefined>;
|
||||
|
||||
export function createSubscriptionRouter() {
|
||||
const subscriptions: JsMessageCallbackMap = {};
|
||||
const subscriptions: FrontendMessageCallbacks = {};
|
||||
const layoutCallbacks: Partial<Record<LayoutTarget, (diffs: WidgetDiff[]) => void>> = {};
|
||||
|
||||
const subscribeJsMessage = <T extends JsMessage, Args extends unknown[]>(messageType: new (...args: Args) => T, callback: JsMessageCallback<T>) => {
|
||||
subscriptions[messageType.name] = callback;
|
||||
const subscribeFrontendMessage = <T extends keyof FrontendMessages>(messageType: T, callback: (data: FrontendMessages[T]) => void) => {
|
||||
subscriptions[messageType] = callback;
|
||||
};
|
||||
|
||||
const unsubscribeJsMessage = <T extends JsMessage>(messageType: new () => T) => {
|
||||
delete subscriptions[messageType.name];
|
||||
const unsubscribeFrontendMessage = (messageType: keyof FrontendMessages) => {
|
||||
delete subscriptions[messageType];
|
||||
};
|
||||
|
||||
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) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(
|
||||
`Received a frontend message of type "${messageType}" but was not able to parse the data. ` +
|
||||
"(Perhaps this message parser isn't exported in `messageMakers` at the bottom of `messages.ts`.)",
|
||||
);
|
||||
return;
|
||||
const subscribeLayoutUpdate = (target: LayoutTarget, callback: (diffs: WidgetDiff[]) => void) => {
|
||||
layoutCallbacks[target] = callback;
|
||||
};
|
||||
|
||||
const unsubscribeLayoutUpdate = (target: LayoutTarget) => {
|
||||
delete layoutCallbacks[target];
|
||||
};
|
||||
|
||||
const handleFrontendMessage = (messageType: keyof FrontendMessages, messageData: Record<string, unknown>) => {
|
||||
// Messages with non-empty data are provided by Serde JSON as an object with one key as the message name, like: { NameOfThisMessage: { ... } }
|
||||
// Messages with empty data are provided by Serde JSON as a string with the message name, like: "NameOfThisMessage"
|
||||
// Here we extract the payload object or use an empty object depending on the situation.
|
||||
const message = messageData[messageType] || {};
|
||||
|
||||
// Resolve the callback lookup and the data to pass, depending on whether this is a layout update or a regular message.
|
||||
// UpdateLayout messages are dispatched to layout-specific callbacks based on the layout target.
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let getCallback: () => ((data: any) => void) | undefined;
|
||||
let callbackData: unknown;
|
||||
let errorLabel: string;
|
||||
if (messageType === "UpdateLayout") {
|
||||
const { layoutTarget, diff } = message as FrontendMessages["UpdateLayout"];
|
||||
getCallback = () => layoutCallbacks[layoutTarget];
|
||||
callbackData = parseWidgetDiffs(diff);
|
||||
errorLabel = `UpdateLayout for layout target "${layoutTarget}"`;
|
||||
} else {
|
||||
getCallback = () => subscriptions[messageType];
|
||||
callbackData = message;
|
||||
errorLabel = messageType;
|
||||
}
|
||||
|
||||
// Checks if the provided `messageMaker` is a class extending `JsMessage`. All classes inheriting from `JsMessage` will have a static readonly `jsMessageMarker` which is `true`.
|
||||
const isJsMessageMaker = (fn: typeof messageMaker): fn is typeof JsMessage => "jsMessageMarker" in fn;
|
||||
const messageIsClass = isJsMessageMaker(messageMaker);
|
||||
|
||||
// Messages with non-empty data are provided by wasm-bindgen as an object with one key as the message name, like: { NameOfThisMessage: { ... } }
|
||||
// Messages with empty data are provided by wasm-bindgen as a string with the message name, like: "NameOfThisMessage"
|
||||
// Here we extract the payload object or use an empty object depending on the situation.
|
||||
const unwrappedMessageData = messageData[messageType] || {};
|
||||
|
||||
// Converts to a `JsMessage` object by turning the JSON message data into an instance of the message class, either automatically or by calling the function that builds it.
|
||||
// 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, 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.
|
||||
// Try to execute the callback. Due to message ordering, the callback may not be registered yet,
|
||||
// so we retry a few times on the next stack frame to give onMount a chance to run.
|
||||
let retries = 0;
|
||||
const callCallback = () => {
|
||||
// It is ok to use constructor.name even with minification since it is used consistently with registerHandler
|
||||
const callback = subscriptions[message.constructor.name];
|
||||
const callback = getCallback();
|
||||
|
||||
// Attempt to call the callback, but try again several times on the next stack frame if it is not yet registered due to message ordering.
|
||||
if (callback) {
|
||||
callback(message);
|
||||
callback(callbackData);
|
||||
} else if (retries <= 3) {
|
||||
retries += 1;
|
||||
setTimeout(callCallback, 0);
|
||||
} else {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(`Received a frontend message of type "${messageType}" but no handler was registered for it from the client.`);
|
||||
console.error(`Received a frontend message of type "${errorLabel}" but no handler was registered for it from the client.`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -70,9 +68,11 @@ export function createSubscriptionRouter() {
|
||||
};
|
||||
|
||||
return {
|
||||
subscribeJsMessage,
|
||||
unsubscribeJsMessage,
|
||||
handleJsMessage,
|
||||
subscribeFrontendMessage,
|
||||
unsubscribeFrontendMessage,
|
||||
subscribeLayoutUpdate,
|
||||
unsubscribeLayoutUpdate,
|
||||
handleFrontendMessage,
|
||||
};
|
||||
}
|
||||
export type SubscriptionRouter = ReturnType<typeof createSubscriptionRouter>;
|
||||
|
||||
Reference in New Issue
Block a user