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
+73
View File
@@ -0,0 +1,73 @@
import type { Editor } from "@graphite/editor";
type ApiResponse = { family: string; variants: string[]; files: Record<string, string> }[];
const FONT_LIST_API = "https://api.graphite.art/font-list";
let currentCleanup: (() => void) | undefined;
let currentArgs: [Editor] | undefined;
export function createFontsManager(editor: Editor) {
currentArgs = [editor];
const abortController = new AbortController();
// Subscribe to process backend events
editor.subscriptions.subscribeFrontendMessage("TriggerFontCatalogLoad", async () => {
try {
const response = await fetch(FONT_LIST_API, { signal: abortController.signal });
if (!response.ok) throw new Error(`Font catalog request failed with status ${response.status}`);
const fontListResponse: { items: ApiResponse } = await response.json();
const fontListData = fontListResponse.items;
const catalog = fontListData.map((font) => {
const styles = font.variants.map((variant) => {
const weight = variant === "regular" || variant === "italic" ? 400 : parseInt(variant, 10);
const italic = variant.endsWith("italic");
const url = font.files[variant].replace("http://", "https://");
return { weight, italic, url };
});
return { name: font.family, styles };
});
editor.handle.onFontCatalogLoad(catalog);
} catch (error) {
if (error instanceof DOMException && error.name === "AbortError") return;
throw error;
}
});
editor.subscriptions.subscribeFrontendMessage("TriggerFontDataLoad", async (data) => {
const { fontFamily, fontStyle } = data.font;
try {
if (!data.url) throw new Error("No URL provided for font data load");
const response = await fetch(data.url, { signal: abortController.signal });
if (!response.ok) throw new Error(`Font data request failed with status ${response.status}`);
const buffer = await response.arrayBuffer();
const bytes = new Uint8Array(buffer);
editor.handle.onFontLoad(fontFamily, fontStyle, bytes);
} catch (error) {
if (error instanceof DOMException && error.name === "AbortError") return;
// eslint-disable-next-line no-console
console.error("Failed to load font:", error);
}
});
function destroy() {
abortController.abort();
editor.subscriptions.unsubscribeFrontendMessage("TriggerFontCatalogLoad");
editor.subscriptions.unsubscribeFrontendMessage("TriggerFontDataLoad");
}
currentCleanup = destroy;
return { destroy };
}
export type FontsManager = ReturnType<typeof createFontsManager>;
// Self-accepting HMR: tear down the old instance and re-create with the new module's code
import.meta.hot?.accept((newModule) => {
currentCleanup?.();
if (currentArgs) newModule?.createFontsManager(...currentArgs);
});