Skip to content

Commit c587c94

Browse files
committed
Refactor SimulationProvider to use functional updates for state management, preventing race conditions during initialization and running of simulations. Improved error handling and state checks to enhance robustness.
1 parent 46a0b7e commit c587c94

File tree

1 file changed

+70
-73
lines changed

1 file changed

+70
-73
lines changed

libs/@hashintel/petrinaut/src/state/simulation-provider.tsx

Lines changed: 70 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -260,100 +260,97 @@ export const SimulationProvider: React.FC<SimulationProviderProps> = ({
260260
};
261261

262262
const initialize: SimulationContextValue["initialize"] = ({ seed, dt }) => {
263-
const currentState = getState();
263+
// Use functional update to ensure we see the latest state at processing time.
264+
// This prevents race conditions where another action (like run()) modifies
265+
// state between when we read it and when the update is processed.
266+
setStateValues((prev) => {
267+
if (prev.state === "Running") {
268+
// Don't overwrite if another action changed state to Running.
269+
// We can't throw inside a state updater, so we just return unchanged.
270+
// The UI should prevent this case, but this is a safety guard.
271+
return prev;
272+
}
264273

265-
if (currentState.state === "Running") {
266-
throw new Error(
267-
"Cannot initialize simulation while it is running. Please reset first.",
268-
);
269-
}
274+
try {
275+
const sdcpn = getSDCPN();
276+
277+
// Check SDCPN validity before building simulation
278+
const checkResult = checkSDCPN(sdcpn);
279+
280+
if (!checkResult.isValid) {
281+
const firstError = checkResult.itemDiagnostics[0]!;
282+
const firstDiagnostic = firstError.diagnostics[0]!;
283+
const errorMessage =
284+
typeof firstDiagnostic.messageText === "string"
285+
? firstDiagnostic.messageText
286+
: ts.flattenDiagnosticMessageText(
287+
firstDiagnostic.messageText,
288+
"\n",
289+
);
290+
291+
return {
292+
...prev,
293+
simulation: null,
294+
state: "Error" as const,
295+
error: `TypeScript error in ${firstError.itemType} (${firstError.itemId}): ${errorMessage}`,
296+
errorItemId: firstError.itemId,
297+
};
298+
}
270299

271-
try {
272-
const sdcpn = getSDCPN();
273-
274-
// Check SDCPN validity before building simulation
275-
const checkResult = checkSDCPN(sdcpn);
276-
277-
if (!checkResult.isValid) {
278-
const firstError = checkResult.itemDiagnostics[0]!;
279-
const firstDiagnostic = firstError.diagnostics[0]!;
280-
const errorMessage =
281-
typeof firstDiagnostic.messageText === "string"
282-
? firstDiagnostic.messageText
283-
: ts.flattenDiagnosticMessageText(
284-
firstDiagnostic.messageText,
285-
"\n",
286-
);
287-
288-
setStateValues({
289-
...currentState,
290-
simulation: null,
291-
state: "Error" as const,
292-
error: `TypeScript error in ${firstError.itemType} (${firstError.itemId}): ${errorMessage}`,
293-
errorItemId: firstError.itemId,
294-
});
295-
} else {
296300
// Build the simulation instance using stored initialMarking and parameterValues
297301
const simulationInstance = buildSimulation({
298302
sdcpn,
299-
initialMarking: currentState.initialMarking,
300-
parameterValues: currentState.parameterValues,
303+
initialMarking: prev.initialMarking,
304+
parameterValues: prev.parameterValues,
301305
seed,
302306
dt,
303307
});
304308

305-
setStateValues({
306-
...currentState,
309+
return {
310+
...prev,
307311
simulation: simulationInstance,
308312
state: "Paused",
309313
error: null,
310314
errorItemId: null,
311315
currentlyViewedFrame: 0,
312-
});
316+
};
317+
} catch (error) {
318+
// eslint-disable-next-line no-console
319+
console.error("Error initializing simulation:", error);
320+
321+
return {
322+
...prev,
323+
simulation: null,
324+
state: "Error",
325+
error:
326+
error instanceof Error
327+
? error.message
328+
: "Unknown error occurred during initialization",
329+
errorItemId: error instanceof SDCPNItemError ? error.itemId : null,
330+
};
313331
}
314-
} catch (error) {
315-
// eslint-disable-next-line no-console
316-
console.error("Error initializing simulation:", error);
317-
318-
setStateValues({
319-
...currentState,
320-
simulation: null,
321-
state: "Error",
322-
error:
323-
error instanceof Error
324-
? error.message
325-
: "Unknown error occurred during initialization",
326-
errorItemId: error instanceof SDCPNItemError ? error.itemId : null,
327-
});
328-
}
332+
});
329333
};
330334

331335
const run: SimulationContextValue["run"] = () => {
332-
const currentState = getState();
333-
334-
if (currentState.state === "Running") {
335-
throw new Error("Cannot run simulation: Simulation is already running.");
336-
}
337-
338-
if (!currentState.simulation) {
339-
throw new Error(
340-
"Cannot run simulation: No simulation initialized. Call initialize() first.",
341-
);
342-
}
336+
// Use functional update to ensure we see the latest state at processing time.
337+
// This prevents race conditions where state changes between validation and update.
338+
setStateValues((prev) => {
339+
// Guard against invalid states - return unchanged if we can't run
340+
if (prev.state === "Running") {
341+
return prev; // Already running
342+
}
343343

344-
if (currentState.state === "Error") {
345-
throw new Error(
346-
"Cannot run simulation: Simulation is in error state. Please reset.",
347-
);
348-
}
344+
if (!prev.simulation) {
345+
return prev; // No simulation initialized
346+
}
349347

350-
if (currentState.state === "Complete") {
351-
throw new Error(
352-
"Cannot run simulation: Simulation is complete. Please reset to run again.",
353-
);
354-
}
348+
if (prev.state === "Error" || prev.state === "Complete") {
349+
return prev; // Can't run from these states
350+
}
355351

356-
setStateValues((prev) => ({ ...prev, state: "Running" }));
352+
return { ...prev, state: "Running" };
353+
});
357354
};
358355

359356
const pause: SimulationContextValue["pause"] = () => {

0 commit comments

Comments
 (0)