Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 76 additions & 7 deletions client/src/hooks/use-compile-and-run.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useCallback, useEffect, type MutableRefObject, type RefObject } from "react";
import { useCallback, useEffect, useLayoutEffect, useRef, type MutableRefObject, type RefObject } from "react";
import { type UseMutationResult } from "@tanstack/react-query";
import { Logger } from "@shared/logger";
import type { IOPinRecord, OutputLine, ParserMessage } from "@shared/schema";
Expand Down Expand Up @@ -173,10 +173,41 @@ interface UseCompileAndRunResult {
startSimulation: () => void;
startSimulationRef: MutableRefObject<(() => void) | null>;
suppressAutoStopOnce: () => void;
/** Call before replacing the workspace or externally loading code. */
invalidatePendingStart: () => void;
}

export function useCompileAndRun(params: CompileAndRunParams): UseCompileAndRunResult {
const controllerState = useSimulatorControllerState();
const startGeneration = useRef(0);
const compileOwners = useRef(new WeakMap<CompileConfig, number>());
const pendingCompileStart = useRef(false);
const resetTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
const invalidatePendingStart = useCallback(() => {
startGeneration.current++;
if (pendingCompileStart.current) {
controllerState.setCompilationStatus("ready");
controllerState.setArduinoCliStatus("idle");
pendingCompileStart.current = false;
}
if (resetTimer.current !== null) clearTimeout(resetTimer.current);
resetTimer.current = null;
}, [controllerState.setCompilationStatus, controllerState.setArduinoCliStatus]);
const isCompileResultCurrent = useCallback((payload: CompileConfig) => {
const owner = compileOwners.current.get(payload);
return owner === undefined || owner === startGeneration.current;
}, []);

// Workspace identity excludes editor contents and the selected tab: ordinary
// navigation/edits keep the captured compile snapshot semantics.
const workspaceIdentity = JSON.stringify([
params.tabs.map((tab) => [tab.id, tab.path ?? tab.name]),
params.sourceProject?.entryFile,
]);
useLayoutEffect(() => {
invalidatePendingStart();
}, [workspaceIdentity, invalidatePendingStart]);
useEffect(() => invalidatePendingStart, [invalidatePendingStart]);

// ------------------------------------------------------------
// UI Feedback Adapter (extrahiert für Schritt 1 von Phase 2.1)
Expand Down Expand Up @@ -212,6 +243,7 @@ export function useCompileAndRun(params: CompileAndRunParams): UseCompileAndRunR
} = useCompileController({
...controllerState,
capabilities: params.capabilities,
isCompileResultCurrent,
// Callbacks
setParserMessages: params.setParserMessages,
setParserPanelDismissed: params.setParserPanelDismissed,
Expand Down Expand Up @@ -248,6 +280,7 @@ export function useCompileAndRun(params: CompileAndRunParams): UseCompileAndRunR
});

const simulation = useSimulationController({
onStartOrStop: invalidatePendingStart,
code: params.code,
hasCompilationErrors,
isModified: params.isModified,
Expand Down Expand Up @@ -278,6 +311,8 @@ export function useCompileAndRun(params: CompileAndRunParams): UseCompileAndRunR
}, [simulation.setCompiledCode]);

const handleCompileAndStart = useCallback(() => {
invalidatePendingStart();
const generation = startGeneration.current;
if (
params.capabilities &&
(!params.capabilities.canCompile || !params.capabilities.canSimulate)
Expand Down Expand Up @@ -315,8 +350,12 @@ export function useCompileAndRun(params: CompileAndRunParams): UseCompileAndRunR

// Compile with custom handlers for compile + start flow
const compilePayload = { code: mainSketchCode, headers, ...(entryFile ? { entryFile } : {}) };
compileOwners.current.set(compilePayload, generation);
pendingCompileStart.current = true;
compileMutation.mutate(compilePayload, {
onSuccess: (data) => {
if (generation !== startGeneration.current) return;
pendingCompileStart.current = false;
logger.info(`[CLIENT] Compile response: ${JSON.stringify(data, null, 2)}`);

if (data.success) {
Expand All @@ -334,15 +373,18 @@ export function useCompileAndRun(params: CompileAndRunParams): UseCompileAndRunR
}
},
onError: () => {
if (generation !== startGeneration.current) return;
pendingCompileStart.current = false;
setCompilationStatus("error");
simulation.setSimulationStatus("idle");
uiFeedback.showCompilationFailedWithErrorsToast();
scheduleCliIdle(setArduinoCliStatus);
},
});
}, [params, clearOutputs, compileMutation, simulation, uiFeedback]);
}, [params, clearOutputs, compileMutation, simulation, uiFeedback, invalidatePendingStart]);

const handleReset = useCallback(() => {
invalidatePendingStart();
if (params.capabilities && !params.capabilities.canSimulate) return;
if (!params.ensureBackendConnected("Reset simulation")) return;
if (simulation.simulationStatus === "running") simulation.handleStop();
Expand All @@ -351,8 +393,10 @@ export function useCompileAndRun(params: CompileAndRunParams): UseCompileAndRunR

uiFeedback.showResettingToast();

setTimeout(() => {
handleCompileAndStart();
const generation = startGeneration.current;
resetTimer.current = setTimeout(() => {
resetTimer.current = null;
if (generation === startGeneration.current) handleCompileAndStart();
}, 100);
}, [
clearOutputs,
Expand All @@ -361,6 +405,7 @@ export function useCompileAndRun(params: CompileAndRunParams): UseCompileAndRunR
params.resetPinUI,
simulation,
uiFeedback,
invalidatePendingStart,
]);

return {
Expand All @@ -377,7 +422,10 @@ export function useCompileAndRun(params: CompileAndRunParams): UseCompileAndRunR
cliOutput,
setCliOutput,
compileMutation,
handleCompile,
handleCompile: () => {
invalidatePendingStart();
handleCompile();
},
handleCompileAndStart,
handleClearCompilationOutput,
clearOutputs,
Expand All @@ -390,8 +438,28 @@ export function useCompileAndRun(params: CompileAndRunParams): UseCompileAndRunR
setSimulationTimeout: simulation.setSimulationTimeout,
dockerGccPhase: controllerState.dockerGccPhase,
setDockerGccPhase: controllerState.setDockerGccPhase,
startMutation: simulation.startMutation,
stopMutation: simulation.stopMutation,
startMutation: {
...simulation.startMutation,
mutate: (...args) => {
invalidatePendingStart();
simulation.startMutation.mutate(...args);
},
mutateAsync: (...args) => {
invalidatePendingStart();
return simulation.startMutation.mutateAsync(...args);
},
},
stopMutation: {
...simulation.stopMutation,
mutate: (...args) => {
invalidatePendingStart();
simulation.stopMutation.mutate(...args);
},
mutateAsync: (...args) => {
invalidatePendingStart();
return simulation.stopMutation.mutateAsync(...args);
},
},
pauseMutation: simulation.pauseMutation,
resumeMutation: simulation.resumeMutation,
handleStart: simulation.handleStart,
Expand All @@ -403,5 +471,6 @@ export function useCompileAndRun(params: CompileAndRunParams): UseCompileAndRunR
startSimulation: simulation.startSimulation,
startSimulationRef: simulation.startSimulationRef,
suppressAutoStopOnce: simulation.suppressAutoStopOnce,
invalidatePendingStart,
};
}
14 changes: 10 additions & 4 deletions client/src/hooks/use-compile-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ export type CompilationErrors = CompilerError[] | string | undefined;

export interface UseCompileControllerParams {
readonly capabilities?: ServerCapabilities;
/** Ignore completions whose compile-to-start owner was invalidated. */
isCompileResultCurrent?: (payload: CompileConfig) => boolean;
// State
compilationStatus: CompilationStatus;
setCompilationStatus: SetState<CompilationStatus>;
Expand Down Expand Up @@ -105,8 +107,10 @@ export function useCompileController(params: UseCompileControllerParams): UseCom

const compileMutation = useMutation<CompileResult, unknown, CompileConfig, unknown>({
mutationFn: async (payload: CompileConfig): Promise<CompileResult> => {
params.setArduinoCliStatus("compiling");
params.setLastCompilationResult(null);
if (params.isCompileResultCurrent?.(payload) !== false) {
params.setArduinoCliStatus("compiling");
params.setLastCompilationResult(null);
}
params.uiFeedback.logCompileRequest(payload.code.length);
const response = await apiRequest("POST", "/api/compile", payload);
const ct = (response.headers.get("content-type") || "").toLowerCase();
Expand All @@ -126,14 +130,16 @@ export function useCompileController(params: UseCompileControllerParams): UseCom
const txt = await response.text();
return { success: false, errors: txt, raw: txt };
},
onSuccess: (data) => {
onSuccess: (data, payload) => {
if (params.isCompileResultCurrent?.(payload) === false) return;
if (data.success) {
handleCompileSuccess(data);
} else {
handleCompileError(data);
}
},
onError: (error: unknown) => {
onError: (error: unknown, payload) => {
if (params.isCompileResultCurrent?.(payload) === false) return;
params.setArduinoCliStatus("error");
params.uiFeedback.triggerCompileErrorGlitch();
if (params.isBackendUnreachableError(error)) {
Expand Down
11 changes: 8 additions & 3 deletions client/src/hooks/use-simulation-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ const logger = new Logger("useSimulationController");

export type SimulationControllerParams = {
readonly capabilities?: ServerCapabilities;
/** Synchronously relinquish pending REST compile-to-start ownership. */
onStartOrStop?: () => void;
code: string;
hasCompilationErrors: boolean;
isModified?: boolean;
Expand Down Expand Up @@ -162,8 +164,9 @@ export function useSimulationController(
});

const startSimulation = useCallback(() => {
params.onStartOrStop?.();
if (canSimulate) startMutation.mutate();
}, [canSimulate, startMutation]);
}, [canSimulate, startMutation, params.onStartOrStop]);
const setCompiledCode = useCallback((code: string) => {
compiledCodeRef.current = code;
}, []);
Expand All @@ -174,15 +177,17 @@ export function useSimulationController(
compiledEntryFileRef.current = entryFile;
}, []);
const handleStart = useCallback(() => {
params.onStartOrStop?.();
if (!canSimulate) return;
if (!params.ensureBackendConnected("Simulation starten")) return;
startSimulation();
}, [canSimulate, params.ensureBackendConnected, startSimulation]);
}, [canSimulate, params.ensureBackendConnected, startSimulation, params.onStartOrStop]);
const handleStop = useCallback(() => {
params.onStartOrStop?.();
if (!canSimulate) return;
if (!params.ensureBackendConnected("Simulation stoppen")) return;
stopMutation.mutate();
}, [canSimulate, params.ensureBackendConnected, stopMutation]);
}, [canSimulate, params.ensureBackendConnected, stopMutation, params.onStartOrStop]);
const handlePause = useCallback(() => {
if (!canSimulate) return;
if (!params.ensureBackendConnected("Simulation pausieren")) return;
Expand Down
12 changes: 11 additions & 1 deletion client/src/hooks/useArduinoSimulatorPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,7 @@ export function useArduinoSimulatorPage() {
handleResume: controllerHandleResume,
handleReset: controllerHandleReset,
suppressAutoStopOnce,
invalidatePendingStart,
} = useCompileAndRun({
editorRef,
tabs,
Expand Down Expand Up @@ -365,6 +366,7 @@ export function useArduinoSimulatorPage() {
// Use centralized output panel hook for all output-related state and callbacks

const onReplaceAllFiles = useCallback(() => {
invalidatePendingStart();
if (simulationStatus === "running") {
sendMessage({ type: "stop_simulation" });
}
Expand All @@ -378,6 +380,7 @@ export function useArduinoSimulatorPage() {
setSimulationStatus("idle");
setHasCompiledOnce(false);
}, [
invalidatePendingStart,
simulationStatus,
sendMessage,
clearOutputs,
Expand All @@ -391,6 +394,7 @@ export function useArduinoSimulatorPage() {
]);

const onLoadExample = useCallback(() => {
invalidatePendingStart();
if (simulationStatus === "running") {
sendMessage({ type: "stop_simulation" });
}
Expand All @@ -409,6 +413,7 @@ export function useArduinoSimulatorPage() {
setSimulationStatus("idle");
setHasCompiledOnce(false);
}, [
invalidatePendingStart,
simulationStatus,
sendMessage,
clearOutputs,
Expand Down Expand Up @@ -734,6 +739,11 @@ export function useArduinoSimulatorPage() {
const externalAllowedOrigin =
globalThis.location.ancestorOrigins?.[0] ?? globalThis.location.origin;

const loadExternalCode = useCallback((nextCode: string) => {
invalidatePendingStart();
setCode(nextCode);
}, [invalidatePendingStart, setCode]);

const { pendingExternalStart } = useSimulatorExternalControl({
allowedOrigin: externalAllowedOrigin,
backendReachable,
Expand All @@ -742,7 +752,7 @@ export function useArduinoSimulatorPage() {
handleStop,
handlePause,
handleResume,
setCode,
setCode: loadExternalCode,
setSimulationStatus,
sendMessage,
pinStates,
Expand Down
Loading
Loading