feat: fortify project saving#1522
Conversation
There was a problem hiding this comment.
Pull request overview
This PR fortifies project saving/autosave behavior across owner (Python/HTML) and Scratch projects by reducing no-op saves, adding a post-save cooldown, deferring autosave during execution, and exposing a unified host API to flush pending autosaves before navigation.
Changes:
- Added initial-project snapshot tracking (components + name + instructions) and updated “changed since load” checks to prevent save-on-open and other no-op saves.
- Introduced owner autosave orchestration (
useOwnerAutoSave) with queuing, in-flight coordination, execution deferral, and a 10s post-success cooldown; Scratch autosave was updated to match this model. - Added a host-facing flush API (web component getters +
flushPendingAutoSave()) and enhanced run-state signaling (Python runners + Scratch VM run-stop events) to avoid autosaving mid-run.
Reviewed changes
Copilot reviewed 22 out of 22 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| src/web-component.js | Exposes autosave flush state and extends “changed since load” to include name/instructions. |
| src/utils/projectHelpers.js | Extends change detection and centralizes initial snapshot syncing. |
| src/utils/projectHelpers.test.js | Adds coverage for name/instructions change detection. |
| src/utils/ownerAutoSaveHostApi.js | Adds combined owner+scratch host autosave API registry and aggregator. |
| src/utils/ownerAutoSaveHostApi.test.js | Tests combined navigation-flush checks and flush sequencing. |
| src/redux/reducers/loadProjectReducers.js | Uses centralized snapshot sync on load and resets initial name/instructions in pending. |
| src/redux/reducers/loadProjectReducers.test.js | Updates expected editor state to include initial name/instructions. |
| src/redux/EditorSlice.js | Adds codeRunInProgress, routes snapshot sync through helper, and exports beginCodeRun. |
| src/redux/EditorSlice.test.js | Updates expected initial editor state to include initial name/instructions. |
| src/PyodideWorker.js | Notifies the UI when a run completes via handleRunComplete. |
| src/hooks/useScratchSaveState.js | Adds cooldown + run deferral + host flush registration + unload/pagehide handling for Scratch autosave. |
| src/hooks/useScratchSaveState.test.js | Adds tests for cooldown queuing, run deferral, flush bypassing cooldown, and host flush state. |
| src/hooks/useProjectPersistence.js | Switches owner autosave to useOwnerAutoSave and extends change detection inputs. |
| src/hooks/useProjectPersistence.test.js | Updates autosave behavior tests (unchanged projects no longer autosave; adds in-flight/cooldown scenarios). |
| src/hooks/useProject.js | Extends cached-project change detection to include initial name/instructions. |
| src/hooks/useOwnerAutoSave.js | New owner autosave orchestration hook with cooldown, queuing, run deferral, and host flush wiring. |
| src/hooks/useOwnerAutoSave.test.js | Adds detailed test coverage for queuing, cooldown, flush behavior, and failure cases. |
| src/components/Editor/Runners/PythonRunner/SkulptRunner/SkulptRunner.jsx | Dispatches beginCodeRun() at run start. |
| src/components/Editor/Runners/PythonRunner/PyodideRunner/PyodideRunner.jsx | Dispatches beginCodeRun() and handles handleRunComplete from the worker. |
| src/components/Editor/Runners/PythonRunner/PyodideRunner/PyodideRunner.test.js | Adds assertions for beginCodeRun and handleRunComplete dispatch behavior. |
| apps/scratch-frame/src/ScratchIntegrationHOC.jsx | Adds Scratch VM PROJECT_RUN_STOP listener and posts “run stopped” event to parent. |
| apps/scratch-frame/src/ScratchIntegrationHOC.test.jsx | Tests the new PROJECT_RUN_STOP listener and posted event. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Track instructions and name in the initial snapshot so autosave only calls the API after real edits, eliminating save-on-open and no-op saves.
Extract useOwnerAutoSave to retry dirty autosaves when a save is already pending, preventing dropped saves without re-debouncing after completion.
Track codeRunInProgress during Pyodide and Skulpt execution so file writes from a run queue autosave until the run completes, avoiding spurious saves.
Add a 10s post-autosave cooldown for Python/HTML owner saves, and flush dirty saves on page hide and unmount so pending edits are not lost.
Expose owner autosave state and an awaitable flushPendingAutoSave method on the web component so host apps can save dirty projects before navigation.
…h on navigation Scratch autosave now follows the same rules as owner autosave: wait out a 10s cooldown after a successful save, queue changes made during a save or while blocks are running, and flush unsaved work before SPA navigation or tab close.
- Wait for any in-flight Redux save before flushing owner autosave, not only saves started by the hook, to avoid concurrent manual/autosave requests - Base beforeunload warnings on unsaved project changes (shouldFlushBeforeNavigation) instead of autosave queue/cooldown state - Snapshot the persisted save payload on saveProject/fulfilled so in-flight edits during a save are not falsely treated as already saved - Stop dispatching codeRunHandled early in Pyodide/Skulpt error and stop paths; rely on run completion instead - Ensure PyodideWorker always posts handleRunComplete in finally, including for non-Python worker errors, so codeRunInProgress cannot remain stuck - Add tests for Redux-save waiting and unload-warning behaviour
a8d82bd to
6bf0ca2
Compare
6bf0ca2 to
0c4a42d
Compare
Disable beforeunload handling inside the scratch-frame iframe so the parent editor owns unsaved-change warnings and navigation flushing. Also fix ScratchIntegrationHOC test store setup.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 59d21f5. Configure here.

Arises from issue: 1543
This is a large PR - might be easier to review by stepping through the commits
Editor UI Autosave Cooldown And Navigation Flush
For full functionality requires the changes from
Problem Summary
The editor was emitting unnecessary
Project - Savedevents and making avoidable save requests across several autosave paths.Supporting figures
Project - Savedevents per minuteThe cooldown is the main lever for reducing save frequency: with a 2s edit debounce and a 10s post-save cooldown, autosave cannot fire more than once every ~10 seconds after a successful save, which is why the expected upper bound is about 6 saves per minute under continuous editing.
Why a cooldown, not just a longer debounce?
Debounce and cooldown solve different problems, and we need both:
A longer debounce alone would not have capped the 20–30 saves/minute problem. A user who types, pauses briefly, types again on a repeating cycle can still trigger a save after every pause. With only a 2s debounce, someone editing in a steady stop–start pattern could easily produce a save every few seconds indefinitely.
Stretching debounce to 10s would reduce save count, but at the cost of poor UX: nothing would save until the user stopped editing for the full window, so brief pauses mid-task would leave work unsaved for too long.
The cooldown gives a hard upper bound on save frequency while keeping the short debounce for day-to-day editing. Changes made during the cooldown are queued, not dropped, and navigation or tab close can still flush immediately via
flushPendingAutoSave(), bypassing the cooldown when the user leaves.Solution Implementation
This PR hardens autosave across Python, HTML, and Scratch projects.
Python / HTML owner autosave
useOwnerAutoSaveflow that tracks queued work, in-flight saves, runtime execution, and cooldown state.Web component host API
flushPendingAutoSave()to force dirty work to save immediately, bypassing debounce and cooldown when needed.Scratch autosave
project-changedevents until load settles; disables the iframe's ownbeforeunloadhandler so the parent owns leave warnings.PROJECT_RUN_STARTandPROJECT_RUN_STOPVM events, so the parent editor avoids saving mid-run and resumes queued autosave work once execution stops.Behaviour Change
What owners are likely to notice when editing a Python or HTML project:
While editing
Scratch projects follow the same cooldown, queuing, and run-deferral rules. Opening a project no longer triggers a save.
Refreshing, closing the tab, or leaving via in-app navigation
pagehide.Example: refreshing or navigating away from a page that has not saved
autosave-page-refresh-and-navigate-check.mov