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
@@ -4,7 +4,7 @@
import { evaluateMathExpression, isPlatformNative } from "@graphite/../wasm/pkg/graphite_wasm";
import type { NumberInputMode, NumberInputIncrementBehavior, ActionShortcut } from "@graphite/../wasm/pkg/graphite_wasm";
import type { Editor } from "@graphite/editor";
import { PRESS_REPEAT_DELAY_MS, PRESS_REPEAT_INTERVAL_MS } from "@graphite/io-managers/input";
import { PRESS_REPEAT_DELAY_MS, PRESS_REPEAT_INTERVAL_MS } from "@graphite/managers/input";
import { browserVersion } from "@graphite/utility-functions/platform";
import { preventEscapeClosingParentFloatingMenu } from "@graphite/components/layout/FloatingMenu.svelte";
@@ -87,6 +87,12 @@
let shiftKeyDown = false;
// Track whether the Ctrl key is currently held down.
let ctrlKeyDown = false;
// Cleanup function for active drag interactions, called on destroy to prevent leaked listeners
let activeDragCleanup: (() => void) | undefined;
// Track the slider abort state for cleanup on destroy
let sliderResetAbortHandler: (() => void) | undefined;
let sliderAbortTimeout1: ReturnType<typeof setTimeout> | undefined;
let sliderAbortTimeout2: ReturnType<typeof setTimeout> | undefined;
$: watchValue(value, unit);
$: sliderStepValue = isInteger ? (step === undefined ? 1 : step) : "any";
@@ -107,10 +113,31 @@
addEventListener("mousemove", trackShiftAndCtrl);
});
onDestroy(() => {
clearTimeout(repeatTimeout);
clearTimeout(sliderAbortTimeout1);
clearTimeout(sliderAbortTimeout2);
activeDragCleanup?.();
// Exit pointer lock if active (non-Safari path)
if (document.pointerLockElement) document.exitPointerLock();
// Remove Safari cursor-hidden workaround class if present
const isSafari = browserVersion().toLowerCase().includes("safari");
if (isSafari) document.body.classList.remove("cursor-hidden");
// Clean up any listeners related to tracking the Shift and Ctrl keys
removeEventListener("keydown", trackShiftAndCtrl);
removeEventListener("keyup", trackShiftAndCtrl);
removeEventListener("mousemove", trackShiftAndCtrl);
clearTimeout(repeatTimeout);
// Clean up any slider-related listeners that may be active
removeEventListener("mousedown", sliderAbortFromMousedown);
removeEventListener("keydown", sliderAbortFromMousedown);
removeEventListener("pointermove", sliderAbortFromDragging);
removeEventListener("keydown", sliderAbortFromDragging);
removeEventListener("keydown", incrementPressAbort);
if (sliderResetAbortHandler) removeEventListener("pointerup", sliderResetAbortHandler);
});
// ===============================
@@ -297,7 +324,7 @@
pressingArrow = false;
clearTimeout(repeatTimeout);
updateValue(initialValueBeforeDragging);
removeEventListener("keydown", onIncrementPointerUp);
removeEventListener("keydown", incrementPressAbort);
}
// =======================================
@@ -333,10 +360,9 @@
alreadyActedGuard = true;
isDragging = true;
beginDrag(e);
removeEventListener("pointermove", onMove);
removeEventListener("pointerup", onUp);
activeDragCleanup?.();
beginDrag(e);
};
// If it's a mouseup, we'll begin editing the text field.
const onUp = () => {
@@ -346,11 +372,15 @@
isDragging = false;
self?.focus();
removeEventListener("pointermove", onMove);
removeEventListener("pointerup", onUp);
activeDragCleanup?.();
};
addEventListener("pointermove", onMove);
addEventListener("pointerup", onUp);
activeDragCleanup = () => {
removeEventListener("pointermove", onMove);
removeEventListener("pointerup", onUp);
activeDragCleanup = undefined;
};
}
function beginDrag(e: PointerEvent) {
@@ -449,16 +479,20 @@
cumulativeDragDelta = 0;
// Clean up the event listeners.
removeEventListener("pointerup", pointerUp);
removeEventListener("pointermove", pointerMove);
removeEventListener("pointerlockmove", pointerLockMove);
if (usePointerLock) document.removeEventListener("pointerlockchange", pointerLockChange);
activeDragCleanup?.();
};
addEventListener("pointerup", pointerUp);
addEventListener("pointermove", pointerMove);
addEventListener("pointerlockmove", pointerLockMove);
if (usePointerLock) document.addEventListener("pointerlockchange", pointerLockChange);
activeDragCleanup = () => {
removeEventListener("pointerup", pointerUp);
removeEventListener("pointermove", pointerMove);
removeEventListener("pointerlockmove", pointerLockMove);
if (usePointerLock) document.removeEventListener("pointerlockchange", pointerLockChange);
activeDragCleanup = undefined;
};
}
function pointerLockMoveUpdate(delta: number, slow: boolean, snapping: boolean, initialValue: number) {
@@ -657,7 +691,7 @@
// End the user's drag by instantaneously disabling and re-enabling the range input element
if (inputRangeElement) inputRangeElement.disabled = true;
setTimeout(() => {
sliderAbortTimeout1 = setTimeout(() => {
if (inputRangeElement) inputRangeElement.disabled = false;
}, 0);
@@ -680,11 +714,13 @@
// dragging the slider, hitting Escape, then releasing the mouse button. This results in being transferred by `onSliderInput()` to the
// "Deciding" state when we should remain in the "Ready" state as set here. (For debugging, this can be visualized in CSS by
// recoloring the fake slider handle, which is shown in the "Deciding" state.)
setTimeout(() => (rangeSliderClickDragState = "Ready"), 0);
sliderAbortTimeout2 = setTimeout(() => (rangeSliderClickDragState = "Ready"), 0);
// Clean up the event listener that was used to call this function.
removeEventListener("pointerup", sliderResetAbort);
sliderResetAbortHandler = undefined;
};
sliderResetAbortHandler = sliderResetAbort;
addEventListener("pointerup", sliderResetAbort);
// Clean up the event listeners that were for tracking an abort while dragging the slider, now that we're no longer dragging it.