From 95f2aa9a9f9ab92cd3953a72069c7bc492bc5fc5 Mon Sep 17 00:00:00 2001 From: David Montero Crespo Date: Fri, 15 May 2026 00:02:05 -0300 Subject: [PATCH] fix(loadExample): clear currentProject before mutating stores MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Critical data-loss bug. Repro: 1. User opens a saved project at //. The page sets useProjectStore.currentProject = { id, slug, ownerUsername, ... }. Auto-save kicks in and starts watching simulator/editor stores. 2. User clicks the "Examples" link, picks an example, hits Run. 3. loadExample mutates useSimulatorStore (setComponents, setWires, addBoard, removeBoard) and useEditorStore (loadFiles). 4. Auto-save sees the change. Its eligibility check finds currentProject still pointing at the user's saved project (we never touched useProjectStore). It debounces a PUT /api/projects/ with the EXAMPLE's components/wires/ files. The user's saved project is overwritten with the example contents. The URL changing to /editor isn't enough — useProjectStore is store state, not router state. ProjectPage / ProjectByIdPage set it on mount; nothing clears it when the user navigates away. Fix: loadExample calls useProjectStore.getState().clearCurrentProject() BEFORE the simulator/editor mutations. autoSaveImpl is subscribed to useProjectStore via subscribe((s, prev) => ... reset() if id changed), and Zustand notifies subscribers synchronously inside set(), so the reset (projectId=null, baseline hash=null) runs in the same tick. Every subsequent setComponents/setWires/loadFiles fires onChange in the hook, which now sees projectId=null and returns early. No PUT ever goes out. The reset is order-sensitive: it must run BEFORE the store mutations or the hook would already have queued a save with the old projectId before we cleared. Comment in the source spells this out so it doesn't get reordered in a future refactor. In-flight saves are not affected: buildSavePayload() snapshots state before its `await updateProject(...)`, so a save that started right before the example load still sends the user's pre-example state to the right project. Worst case: the save completes after clear, and the hook quietly returns idle. Build verified (vite OSS+pro, 285 SEO pages). Co-Authored-By: Claude Opus 4.7 (1M context) --- frontend/src/utils/loadExample.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/frontend/src/utils/loadExample.ts b/frontend/src/utils/loadExample.ts index 67e3e6aa..045bbf8c 100644 --- a/frontend/src/utils/loadExample.ts +++ b/frontend/src/utils/loadExample.ts @@ -8,6 +8,7 @@ import type { BoardKind } from '../types/board'; import { useEditorStore } from '../store/useEditorStore'; import { useSimulatorStore, DEFAULT_BOARD_POSITION } from '../store/useSimulatorStore'; import { useElectricalStore } from '../store/useElectricalStore'; +import { useProjectStore } from '../store/useProjectStore'; import { useVfsStore } from '../store/useVfsStore'; import { isBoardComponent } from './boardPinMapping'; import { getInstalledLibraries, installLibrary } from '../services/libraryService'; @@ -57,6 +58,22 @@ export async function loadExample( ): Promise { trackOpenExample(example.title); + // CRITICAL — clear currentProject FIRST, before touching any other store. + // + // Otherwise: user has a saved project open (currentProject = { id, slug, …}), + // navigates to /examples, clicks an example. We mutate the simulator + + // editor stores below; the auto-save hook is still subscribed and still + // thinks the active project is the user's saved one. It debounces a + // PUT /api/projects/ with the example's components/wires/files + // and OVERWRITES the user's saved project with the example contents. + // + // The auto-save hook is subscribed to useProjectStore and resets its + // baseline (projectId=null, lastSavedHash=null) whenever currentProject?.id + // changes. Clearing here BEFORE the mutations below guarantees the hook + // sees null as projectId during every subsequent simulator/editor change, + // so no PUT goes out. + useProjectStore.getState().clearCurrentProject(); + // Loading a new example always starts unpaused — otherwise the canvas // would open with every LED frozen at the previous example's state. useElectricalStore.getState().setPaused(false);