Fix frontend HMR so the page doesn't break upon saving TS files and restructure frontend architecture (#3871)

* Clean up component setup/tear-down side effects

* Clean up more component setup/tear-down side effects

* Remove nonfunctional debouncer

* Clean up even more component setup/tear-down side effects

* Reuse backend state

* Fix HMR for IO Managers and for State Providers

* Rename IO Managers -> Managers and State Providers -> Stores

* Restructure and partially flatten managers/stores

* Code review fixes

* Review fixes
This commit is contained in:
Keavon Chambers
2026-03-19 13:38:41 -07:00
committed by GitHub
parent eb562146ce
commit 124b17f609
54 changed files with 1385 additions and 931 deletions
+115
View File
@@ -0,0 +1,115 @@
import { tick } from "svelte";
import { writable } from "svelte/store";
import type { Writable } from "svelte/store";
import type { Layout } from "@graphite/../wasm/pkg/graphite_wasm";
import type { Editor } from "@graphite/editor";
import { patchLayout } from "@graphite/utility-functions/widgets";
type DocumentStoreState = {
toolOptionsLayout: Layout;
documentBarLayout: Layout;
toolShelfLayout: Layout;
workingColorsLayout: Layout;
nodeGraphControlBarLayout: Layout;
graphViewOverlayOpen: boolean;
fadeArtwork: number;
};
const initialState: DocumentStoreState = {
toolOptionsLayout: [],
documentBarLayout: [],
toolShelfLayout: [],
workingColorsLayout: [],
nodeGraphControlBarLayout: [],
graphViewOverlayOpen: false,
fadeArtwork: 100,
};
// Store state persisted across HMR to maintain reactive subscriptions in the component tree
const store: Writable<DocumentStoreState> = import.meta.hot?.data?.store || writable<DocumentStoreState>(initialState);
if (import.meta.hot) import.meta.hot.data.store = store;
const { subscribe, update } = store;
export function createDocumentStore(editor: Editor) {
// Update layouts
editor.subscriptions.subscribeFrontendMessage("UpdateGraphFadeArtwork", (data) => {
update((state) => {
state.fadeArtwork = data.percentage;
return state;
});
});
editor.subscriptions.subscribeLayoutUpdate("ToolOptions", async (data) => {
await tick();
update((state) => {
patchLayout(state.toolOptionsLayout, data);
return state;
});
});
editor.subscriptions.subscribeLayoutUpdate("DocumentBar", async (data) => {
await tick();
update((state) => {
patchLayout(state.documentBarLayout, data);
return state;
});
});
editor.subscriptions.subscribeLayoutUpdate("ToolShelf", async (data) => {
await tick();
update((state) => {
patchLayout(state.toolShelfLayout, data);
return state;
});
});
editor.subscriptions.subscribeLayoutUpdate("WorkingColors", async (data) => {
await tick();
update((state) => {
patchLayout(state.workingColorsLayout, data);
return state;
});
});
editor.subscriptions.subscribeLayoutUpdate("NodeGraphControlBar", async (data) => {
await tick();
update((state) => {
patchLayout(state.nodeGraphControlBarLayout, data);
return state;
});
});
// Show or hide the graph view overlay
editor.subscriptions.subscribeFrontendMessage("UpdateGraphViewOverlay", (data) => {
update((state) => {
state.graphViewOverlayOpen = data.open;
return state;
});
});
function destroy() {
editor.subscriptions.unsubscribeFrontendMessage("UpdateGraphFadeArtwork");
editor.subscriptions.unsubscribeFrontendMessage("UpdateGraphViewOverlay");
editor.subscriptions.unsubscribeLayoutUpdate("ToolOptions");
editor.subscriptions.unsubscribeLayoutUpdate("DocumentBar");
editor.subscriptions.unsubscribeLayoutUpdate("ToolShelf");
editor.subscriptions.unsubscribeLayoutUpdate("WorkingColors");
editor.subscriptions.unsubscribeLayoutUpdate("NodeGraphControlBar");
}
currentCleanup = destroy;
currentArgs = [editor];
return {
subscribe,
destroy,
};
}
export type DocumentStore = ReturnType<typeof createDocumentStore>;
// Self-accepting HMR: tear down the old instance and re-create with the new module's code
let currentCleanup: (() => void) | undefined;
let currentArgs: [Editor] | undefined;
import.meta.hot?.accept((newModule) => {
currentCleanup?.();
if (currentArgs) newModule?.createDocumentStore(...currentArgs);
});