Skip to content

Commit ed7987c

Browse files
authored
Split apart the frontend Editor type into SubscriptionsRouter and EditorHandle (#3923)
* Remove the unused Editor.raw/wasmMemory/wasmImport * Split out Editor.subscriptions * Replace editor.handle.* with editor.* (1 of 2) * Replace editor.handle.* with editor.* (2 of 2) * Replace Editor typedef with EditorHandle import * Pluralize subscription-router and rename subscriptionsRef->subscriptionsRouter and editorRef->editorHandle * Remove editor.ts * Update the readme * Fix demo art loading bug
1 parent 64fd12a commit ed7987c

40 files changed

+553
-588
lines changed

frontend/src/App.svelte

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,42 @@
11
<script lang="ts">
22
import { onMount, onDestroy } from "svelte";
33
4-
import { initWasm, createEditor } from "@graphite/editor";
5-
import type { Editor as GraphiteEditor } from "@graphite/editor";
4+
import init, { EditorHandle, receiveNativeMessage } from "@graphite/../wasm/pkg/graphite_wasm";
5+
import type { FrontendMessage } from "@graphite/../wasm/pkg/graphite_wasm";
6+
import { loadDemoArtwork } from "@graphite/utility-functions/network";
7+
import { operatingSystem } from "@graphite/utility-functions/platform";
8+
import { createSubscriptionsRouter } from "/src/subscriptions-router";
9+
import type { MessageName, SubscriptionsRouter } from "/src/subscriptions-router";
610
711
import Editor from "@graphite/components/Editor.svelte";
812
9-
let editor: GraphiteEditor | undefined = undefined;
13+
let subscriptions: SubscriptionsRouter | undefined = undefined;
14+
let editor: EditorHandle | undefined = undefined;
1015
1116
onMount(async () => {
12-
await initWasm();
17+
// Initialize the Wasm module
18+
const wasm = await init();
19+
for (const [name, f] of Object.entries(wasm)) {
20+
if (name.startsWith("__node_registry")) f();
21+
}
22+
window.imageCanvases = {};
23+
window.receiveNativeMessage = receiveNativeMessage;
1324
14-
editor = createEditor();
25+
// Create the editor and subscriptions router
26+
const randomSeed = BigInt(Math.floor(Math.random() * Number.MAX_SAFE_INTEGER));
27+
subscriptions = createSubscriptionsRouter();
28+
editor = EditorHandle.create(operatingSystem(), randomSeed, (messageType: MessageName, messageData: FrontendMessage) => {
29+
subscriptions?.handleFrontendMessage(messageType, messageData);
30+
});
31+
32+
await loadDemoArtwork(editor);
1533
});
1634
1735
onDestroy(() => {
18-
editor?.destroy();
36+
editor?.free();
1937
});
2038
</script>
2139

22-
{#if editor !== undefined}
23-
<Editor {editor} />
40+
{#if subscriptions !== undefined && editor !== undefined}
41+
<Editor {subscriptions} {editor} />
2442
{/if}

frontend/src/README.md

Lines changed: 11 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,50 +2,44 @@
22

33
## Svelte components: `components/`
44

5-
Svelte components that build the Graphite editor GUI. These each contain a TypeScript section, a Svelte-templated HTML template section, and an SCSS stylesheet section. The aim is to avoid implementing much editor business logic here, just enough to make things interactive and communicate to the backend where the real business logic should occur.
5+
Svelte components that build the Graphite editor GUI from layouts, panels, widgets, and menus. These each contain a TypeScript section, a Svelte-templated HTML template section, and an SCSS stylesheet section. The aim is to avoid implementing much editor business logic here, just enough to make things interactive and communicate to the backend where the real business logic should occur.
66

77
## Managers: `managers/`
88

9-
TypeScript files which manage the input/output of browser APIs and link this functionality with the editor backend. These files subscribe to backend messages to execute JS APIs, and in response to these APIs or user interactions, they may call functions into the backend (defined in `/frontend/wasm/editor_api.rs`).
9+
TypeScript files, constructed by the editor frontend, which manage the input/output of browser APIs and link this functionality with the editor backend. These files subscribe to frontend messages to execute JS APIs, and in response to these APIs or user interactions, they may call functions in the backend (defined in `/frontend/wasm/editor_api.rs`).
1010

11-
Each manager module exports a factory function (e.g. `createClipboardManager(editor)`) that sets up message subscriptions and returns a `{ destroy }` object. In `Editor.svelte`, each manager is created at startup and its `destroy()` method is called on unmount to clean up subscriptions and side-effects (e.g. event listeners). Managers use self-accepting HMR to tear down and re-create with updated code during development.
11+
Each manager module stores its dependencies (like `subscriptionsRouter` and `editorHandle`) in module-level variables and exports a `create*()` and `destroy*()` function pair. `Editor.svelte` calls each `create*()` constructor in its `onMount` and calls each `destroy*()` in its `onDestroy`. Managers replace themselves during HMR updates if they are modified live during development.
1212

1313
## Stores: `stores/`
1414

15-
TypeScript files which provide reactive state to Svelte components. Each module persists a Svelte writable store at module level (surviving HMR via `import.meta.hot.data`) and exports a factory function (e.g. `createDialogStore(editor)`) that sets up backend message subscriptions and returns an object containing the store's `subscribe` method, any action methods for components to call, and a `destroy` method.
15+
TypeScript files, constructed by the editor frontend, which provide reactive state to Svelte components. Each module persists a Svelte writable store at module level (surviving HMR via `import.meta.hot.data`) and exports a `create*()` function that sets up frontend message subscriptions and returns `{ subscribe }` (the shape required by Svelte's custom store contract). A corresponding `destroy*()` function is also exported. Some stores also export standalone action functions (like `createCrashDialog()` or `toggleFullscreen()`) as module-level exports.
1616

17-
In `Editor.svelte`, each store is created and passed to Svelte's `setContext()`. Components access stores via `getContext<DialogStore>("dialog")` and use the `subscribe` method for reactive state and action methods (like `createCrashDialog()`) to trigger state changes.
17+
In `Editor.svelte`, each store is created synchronously during component initialization (not in `onMount`, since child components need `getContext` access during their own initialization) and passed to Svelte's `setContext()`. Components access stores via calls like `getContext<DialogStore>("dialog")`. Unlike managers, stores do not replace themselves during HMR; instead, `Editor.svelte` is remounted to replace them entirely.
1818

1919
## *Managers vs. stores*
2020

21-
*Both managers and stores subscribe to backend messages and may interact with browser APIs. The difference is that stores expose reactive state to components via `setContext()`/`getContext()`, while managers are self-contained systems that operate for the lifetime of the application and aren't accessed by Svelte components.*
21+
*Both managers and stores subscribe to frontend messages and may interact with browser APIs. The difference is that stores expose reactive state to components via `setContext()`/`getContext()`, while managers are self-contained systems that operate for the lifetime of the application and aren't accessed by Svelte components.*
2222

2323
## Utility functions: `utility-functions/`
2424

2525
TypeScript files which define and `export` individual helper functions for use elsewhere in the codebase. These files should not persist state outside each function.
2626

27-
## Wasm editor: `editor.ts`
27+
## Subscriptions router: `subscriptions-router.ts`
2828

29-
Instantiates the Wasm and editor backend instances. The function `initWasm()` asynchronously constructs and initializes an instance of the Wasm bindings JS module provided by wasm-bindgen/wasm-pack. The function `createEditor()` constructs an instance of the editor backend. In theory there could be multiple editor instances sharing the same Wasm module instance. The function returns an object where `raw` is the Wasm memory, `handle` provides access to callable backend functions, and `subscriptions` is the subscription router (described below).
30-
31-
`initWasm()` occurs in `main.ts` right before the Svelte application is mounted, then `createEditor()` is run in `Editor.svelte` during the Svelte app's creation. Similarly to the stores described above, the editor is given via `setContext()` so other components can get it via `getContext` and call functions on `editor.handle` or `editor.subscriptions`.
32-
33-
## Subscription router: `subscription-router.ts`
34-
35-
Associates messages from the backend with subscribers in the frontend, and routes messages to subscriber callbacks. This module provides a `subscribeFrontendMessage(messageType, callback)` function which JS code throughout the frontend can call to be registered as the exclusive handler for a chosen message type. The router's other function, `handleFrontendMessage(messageType, messageData)`, is called via the callback passed to `EditorHandle.create()` in `editor.ts` when the backend sends a `FrontendMessage`. When this occurs, the subscription router delivers the message to the subscriber by executing its registered `callback` function.
29+
Associates messages from the backend with subscribers in the frontend, and routes messages to subscriber callbacks. This module provides a `subscribeFrontendMessage(messageType, callback)` function which JS code throughout the frontend can call to be registered as the exclusive handler for a chosen message type. The router's other function, `handleFrontendMessage(messageType, messageData)`, is called via the callback passed to `EditorHandle.create()` in `App.svelte` when the backend sends a `FrontendMessage`. When this occurs, the subscriptions router delivers the message to the subscriber by executing its registered `callback` function.
3630

3731
## Svelte app entry point: `App.svelte`
3832

39-
The entry point for the Svelte application.
33+
The entry point for the Svelte application. Initializes the Wasm module, creates the `EditorHandle` backend instance and the subscriptions router, and renders `Editor.svelte` once both are ready. The `EditorHandle` is the wasm-bindgen interface to the Rust editor backend (defined in `/frontend/wasm/editor_api.rs`), providing access to callable backend functions. Both the editor and subscriptions router are passed as props to `Editor.svelte` and set as Svelte contexts for use throughout the component tree.
4034

4135
## Editor base instance: `Editor.svelte`
4236

43-
This is where we define global CSS style rules, construct all stores and managers with the editor instance, set store contexts for component access, and clean up all `destroy()` methods on unmount.
37+
This is where we define global CSS style rules, construct all stores and managers, set store contexts for component access, and call each module's `destroy*()` function on unmount (on HMR during development).
4438

4539
## Global type augmentations: `global.d.ts`
4640

4741
Extends built-in browser type definitions using TypeScript's interface merging. This includes Graphite's custom properties on the `window` object, custom events like `pointerlockmove`, and experimental browser APIs not yet in TypeScript's standard library. New custom events or non-standard browser APIs used by the frontend should be declared here.
4842

4943
## JS bundle entry point: `main.ts`
5044

51-
The entry point for the entire project's code bundle. Here we simply mount the Svelte application with `export default mount(App, { target: document.body });`.
45+
The entry point for the entire project's code bundle. Mounts the Svelte application with `export default mount(App, { target: document.body })`.

frontend/src/components/Editor.svelte

Lines changed: 22 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<script lang="ts">
22
import { onMount, onDestroy, setContext } from "svelte";
33
4-
import type { Editor } from "@graphite/editor";
4+
import type { EditorHandle } from "@graphite/../wasm/pkg/graphite_wasm";
55
import { createClipboardManager, destroyClipboardManager } from "@graphite/managers/clipboard";
66
import { createFontsManager, destroyFontsManager } from "@graphite/managers/fonts";
77
import { createHyperlinkManager, destroyHyperlinkManager } from "@graphite/managers/hyperlink";
@@ -16,39 +16,42 @@
1616
import { createNodeGraphStore, destroyNodeGraphStore } from "@graphite/stores/node-graph";
1717
import { createPortfolioStore, destroyPortfolioStore } from "@graphite/stores/portfolio";
1818
import { createTooltipStore, destroyTooltipStore } from "@graphite/stores/tooltip";
19+
import type { SubscriptionsRouter } from "/src/subscriptions-router";
1920
2021
import MainWindow from "@graphite/components/window/MainWindow.svelte";
2122
22-
// Graphite Wasm editor
23-
export let editor: Editor;
23+
// Graphite Wasm editor and subscriptions router
24+
export let subscriptions: SubscriptionsRouter;
25+
export let editor: EditorHandle;
26+
setContext("subscriptions", subscriptions);
2427
setContext("editor", editor);
2528
2629
const stores = {
27-
dialog: createDialogStore(editor),
28-
tooltip: createTooltipStore(editor),
29-
document: createDocumentStore(editor),
30-
fullscreen: createFullscreenStore(editor),
31-
nodeGraph: createNodeGraphStore(editor),
32-
portfolio: createPortfolioStore(editor),
33-
appWindow: createAppWindowStore(editor),
30+
dialog: createDialogStore(subscriptions, editor),
31+
tooltip: createTooltipStore(subscriptions),
32+
document: createDocumentStore(subscriptions),
33+
fullscreen: createFullscreenStore(subscriptions),
34+
nodeGraph: createNodeGraphStore(subscriptions),
35+
portfolio: createPortfolioStore(subscriptions, editor),
36+
appWindow: createAppWindowStore(subscriptions),
3437
};
3538
Object.entries(stores).forEach(([key, store]) => setContext(key, store));
3639
3740
onMount(() => {
38-
createClipboardManager(editor);
39-
createHyperlinkManager(editor);
40-
createLocalizationManager(editor);
41-
createPanicManager(editor);
42-
createPersistenceManager(editor, stores.portfolio);
43-
createFontsManager(editor);
44-
createInputManager(editor, stores.dialog, stores.portfolio, stores.document);
41+
createClipboardManager(subscriptions, editor);
42+
createHyperlinkManager(subscriptions);
43+
createLocalizationManager(subscriptions, editor);
44+
createPanicManager(subscriptions);
45+
createPersistenceManager(subscriptions, editor, stores.portfolio);
46+
createFontsManager(subscriptions, editor);
47+
createInputManager(subscriptions, editor, stores.dialog, stores.portfolio, stores.document);
4548
4649
// Initialize certain setup tasks required by the editor backend to be ready for the user now that the frontend is ready.
4750
// The backend handles idempotency, so this is safe to call again during HMR re-mounts.
48-
editor.handle.initAfterFrontendReady();
51+
editor.initAfterFrontendReady();
4952
5053
// Re-send all UI layouts from Rust so the frontend has them after an HMR re-mount
51-
editor.handle.resendAllLayouts();
54+
editor.resendAllLayouts();
5255
});
5356
5457
onDestroy(() => {

frontend/src/components/floating-menus/Tooltip.svelte

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
<script lang="ts">
22
import { getContext } from "svelte";
33
4-
import type { LabeledShortcut } from "@graphite/../wasm/pkg/graphite_wasm";
5-
import type { Editor } from "@graphite/editor";
4+
import type { EditorHandle, LabeledShortcut } from "@graphite/../wasm/pkg/graphite_wasm";
65
import type { TooltipStore } from "@graphite/stores/tooltip";
76
87
import FloatingMenu from "@graphite/components/layout/FloatingMenu.svelte";
@@ -11,7 +10,7 @@
1110
import TextLabel from "@graphite/components/widgets/labels/TextLabel.svelte";
1211
1312
const tooltip = getContext<TooltipStore>("tooltip");
14-
const editor = getContext<Editor>("editor");
13+
const editor = getContext<EditorHandle>("editor");
1514
1615
let self: FloatingMenu | undefined;
1716
@@ -32,7 +31,7 @@
3231
3332
// TODO: Once all TODOs are replaced with real text, remove this function
3433
function filterTodo(text: string | undefined): string | undefined {
35-
if (text?.trim().toUpperCase() === "TODO" && !editor.handle.inDevelopmentMode()) return "";
34+
if (text?.trim().toUpperCase() === "TODO" && !editor.inDevelopmentMode()) return "";
3635
return text;
3736
}
3837

frontend/src/components/panels/Data.svelte

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,25 @@
22
import { getContext, onMount, onDestroy } from "svelte";
33
44
import type { Layout } from "@graphite/../wasm/pkg/graphite_wasm";
5-
import type { Editor } from "@graphite/editor";
5+
import type { SubscriptionsRouter } from "/src/subscriptions-router";
66
import { patchLayout } from "@graphite/utility-functions/widgets";
77
88
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
99
import WidgetLayout from "@graphite/components/widgets/WidgetLayout.svelte";
1010
11-
const editor = getContext<Editor>("editor");
11+
const subscriptions = getContext<SubscriptionsRouter>("subscriptions");
1212
1313
let dataPanelLayout: Layout = [];
1414
1515
onMount(() => {
16-
editor.subscriptions.subscribeLayoutUpdate("DataPanel", (data) => {
16+
subscriptions.subscribeLayoutUpdate("DataPanel", (data) => {
1717
patchLayout(dataPanelLayout, data);
1818
dataPanelLayout = dataPanelLayout;
1919
});
2020
});
2121
2222
onDestroy(() => {
23-
editor.subscriptions.unsubscribeLayoutUpdate("DataPanel");
23+
subscriptions.unsubscribeLayoutUpdate("DataPanel");
2424
});
2525
</script>
2626

0 commit comments

Comments
 (0)