Skip to content

feat: fortify project saving#1522

Open
DNR500 wants to merge 15 commits into
mainfrom
1543-fortify-saved-tracking-events
Open

feat: fortify project saving#1522
DNR500 wants to merge 15 commits into
mainfrom
1543-fortify-saved-tracking-events

Conversation

@DNR500

@DNR500 DNR500 commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

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 - Saved events and making avoidable save requests across several autosave paths.

  • No-op saves on load: Autosave could fire immediately after opening a project (Python, HTML, and Scratch), even when nothing had changed.
  • Too many saves during active editing: Repeated edits could trigger autosave too aggressively. For Python and HTML projects, intermittent typing could produce 20–30 save events per minute.
  • Saves during in-flight requests: Autosave could attempt another save while a previous save was still pending, increasing the chance of dropped or duplicated work.
  • Saves during Python execution: Autosave could run while Pyodide or Skulpt code was still executing, causing saves to race with runtime/file updates.
  • Scratch autosave had the same class of problems: In the iframe-based Scratch flow, repeated project changes, edits during an in-flight save, and edits while blocks were running could all lead to extra or poorly timed save attempts.
  • No host flush before navigation: Host apps had no shared way to ask the editor to flush pending autosave work before navigation, so dirty changes could remain behind debounce or cooldown when the user moved away.

Supporting figures

Scenario Before After (this PR)
Python / HTML autosave during intermittent typing 20–30 Project - Saved events per minute Capped at ~6 per minute (10s cooldown after each successful autosave)

The 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:

  • Debounce (2s) waits until the user pauses editing before considering a save. It keeps the editor responsive — work is persisted soon after a short break in typing, not only after the user stops for a long time.
  • Cooldown (10s) starts after a successful save and caps how often the API is called, regardless of edit rhythm.

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

  • Tracks whether the project has actually changed from the initial loaded snapshot, including project name and instructions.
  • Only calls the API after real edits, which prevents save-on-open and other no-op saves.
  • Moved into a dedicated useOwnerAutoSave flow that tracks queued work, in-flight saves, runtime execution, and cooldown state.
  • Queues new dirty changes when a save is already pending or Python code is running, then saves once the current operation finishes.
  • Waits out a 10s cooldown after a successful autosave before sending another request.

Web component host API

  • Exposes autosave state to host applications through an awaitable flush API.
  • Lets host apps check whether the editor should save before navigation.
  • Provides flushPendingAutoSave() to force dirty work to save immediately, bypassing debounce and cooldown when needed.
  • Combines owner autosave and Scratch autosave state, so host apps use one navigation-flush path regardless of project type.

Scratch autosave

  • Follows the same behaviour as owner autosave: tracks dirty state, queues changes during in-flight saves, and applies the same 10s cooldown after successful autosaves.
  • Gates iframe project-changed events until load settles; disables the iframe's own beforeunload handler so the parent owns leave warnings.
  • Defers saves while Scratch blocks are running, and flushes pending work on navigation or tab close.
  • Surfaces run state from the Scratch iframe by listening for both PROJECT_RUN_START and PROJECT_RUN_STOP VM 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

  • Opening a project and making no changes no longer triggers a save or a “Saved” status update.
  • After a real edit, autosave still kicks in following a short pause in typing (about 2 seconds).
  • The project bar shows “Saving…” while a save is in flight, then “Saved now” (or a relative time such as “Saved 10 seconds ago”) once it completes.
  • If the user keeps editing in a stop–start pattern, saves happen less often than before. After a successful autosave, the next one will not fire for up to 10 seconds, even if the user pauses and edits again during that window.
  • During that cooldown, the save status may still read “Saved X ago” even though newer edits are queued in the background. Those changes are not lost; they are saved when the cooldown ends, when the user clicks Save, or when the editor flushes on navigation.
  • Clicking Run defers autosave until the run finishes. The user may not see a new “Saved” update until execution has stopped and the queued save completes.

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

  • On refresh or tab close with unsaved changes, the browser shows its native “Leave site?” dialog (wording is browser-controlled, not a custom in-app modal). The editor also attempts to flush pending autosave via pagehide.
  • In-app navigation (e.g. “Your projects” in standalone) is handled silently by the host: it flushes via the host API with no leave modal.

Example: refreshing or navigating away from a page that has not saved

autosave-page-refresh-and-navigate-check.mov

Copilot AI review requested due to automatic review settings July 6, 2026 17:57
@DNR500 DNR500 temporarily deployed to previews/1522/merge July 6, 2026 17:57 — with GitHub Actions Inactive

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/hooks/useOwnerAutoSave.js Outdated
Comment thread src/hooks/useOwnerAutoSave.js
Comment thread src/hooks/useOwnerAutoSave.js Outdated
Comment thread src/PyodideWorker.js Outdated
Comment thread src/hooks/useOwnerAutoSave.js Outdated
@DNR500 DNR500 marked this pull request as draft July 7, 2026 08:22
@DNR500 DNR500 temporarily deployed to previews/1522/merge July 7, 2026 15:56 — with GitHub Actions Inactive
DNR500 added 9 commits July 7, 2026 17:59
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
@DNR500 DNR500 force-pushed the 1543-fortify-saved-tracking-events branch from a8d82bd to 6bf0ca2 Compare July 7, 2026 15:59
@DNR500 DNR500 temporarily deployed to previews/1522/merge July 7, 2026 15:59 — with GitHub Actions Inactive
@DNR500 DNR500 force-pushed the 1543-fortify-saved-tracking-events branch from 6bf0ca2 to 0c4a42d Compare July 7, 2026 16:22
@DNR500 DNR500 temporarily deployed to previews/1522/merge July 7, 2026 16:22 — with GitHub Actions Inactive
@DNR500 DNR500 marked this pull request as ready for review July 7, 2026 16:27
DNR500 added 2 commits July 7, 2026 19:11
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.
@DNR500 DNR500 temporarily deployed to previews/1522/merge July 7, 2026 17:30 — with GitHub Actions Inactive

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants