Skip to content

fix(project): make project saves atomic#741

Merged
meiiie merged 3 commits into
mainfrom
fix/project-atomic-save
Jul 11, 2026
Merged

fix(project): make project saves atomic#741
meiiie merged 3 commits into
mainfrom
fix/project-atomic-save

Conversation

@meiiie

@meiiie meiiie commented Jul 10, 2026

Copy link
Copy Markdown
Collaborator

Description

Makes all project JSON save routes commit complete files instead of overwriting the active file in
place.

  • writes and syncs an exclusive same-directory temporary file
  • preserves the previous complete generation as <project>.bak before replacement
  • serializes overlapping saves to the same path and cleans handled-failure artifacts
  • preserves existing POSIX permission bits and syncs directory metadata where supported

Motivation

The three save handlers used fs.writeFile directly on the active project. An interrupted write
could therefore truncate the user's only .recordly file. Same-directory replacement ensures the
active path contains either the previous or the next complete JSON generation; pre-commit failures
leave it unchanged.

Type of Change

  • Bug Fix

Related Issue(s)

No linked issue. Independent of draft PRs #737-#740.

Testing Guide

  • Electron 39.2.7 / Node 22.21.1 focused tests: 15 passed, 1 Windows-only POSIX-mode skip
  • npx biome lint on the three changed files
  • npx tsc --noEmit --noUnusedLocals false
  • npm test -- --reporter=dot: 97/97 files, 846 passed, 1 skipped
  • production renderer/main/preload build plus Electron-main normalize/smoke

Default tsc --noEmit retains only the current-main unused import failure addressed by #737.

Risk / limits

Verified on Windows with fault, cleanup, concurrency, full-suite, and production-build gates.
Linux/macOS and physical interruption were not runtime-tested. The POSIX mode test runs only off
Windows. Automatic .bak recovery UX is intentionally separate; the existing All Files picker can
open a backup manually. No project schema or renderer contract changes.

Checklist

  • I have performed a self-review of my code.
  • No UI change; screenshots/video are not applicable.

Summary by CodeRabbit

  • New Features

    • Project files are now saved atomically to reduce the risk of corruption during interruptions or overlapping saves.
    • Prior versions are kept as backups when replacing existing project files.
  • Bug Fixes

    • Improved reliability and recovery behavior when overwrites or backup steps fail, including correct retry handling.
    • Backup/version handling is more consistent across platforms, preserving file permissions on non-Windows systems.
    • Automatic cleanup prevents leftover temporary save artifacts.
  • Tests

    • Added comprehensive coverage to validate atomic write/backup behavior, concurrency ordering, and artifact cleanup.

@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: fc4d8307-d2b7-4425-a360-2f453b3907c5

📥 Commits

Reviewing files that changed from the base of the PR and between cf4416a and 62954f0.

📒 Files selected for processing (3)
  • electron/ipc/project/atomicSave.test.ts
  • electron/ipc/project/atomicSave.ts
  • electron/ipc/register/project.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • electron/ipc/register/project.ts
  • electron/ipc/project/atomicSave.ts

📝 Walkthrough

Walkthrough

Project persistence now uses a serialized atomic-write helper that maintains backups, syncs filesystem changes, cleans temporary files, and is integrated into all project save paths. Tests cover replacement, failures, concurrency, cleanup, and permission preservation.

Changes

Atomic project persistence

Layer / File(s) Summary
Atomic write engine
electron/ipc/project/atomicSave.ts
Adds queued atomic writes with temporary files, backup generation preservation, filesystem syncing, permission retention, and cleanup.
Project save integration
electron/ipc/register/project.ts
Routes trusted saves, dialog saves, and named overwrites through the atomic writer and removes the correct backup during rename cleanup.
Atomic write validation
electron/ipc/project/atomicSave.test.ts
Tests initial writes, stale-backup removal, generation preservation, failed commits, concurrent writes, temporary-file cleanup, and permission retention.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ProjectSaveIPC
  participant writeProjectFileAtomically
  participant NodeFilesystem
  ProjectSaveIPC->>writeProjectFileAtomically: submit project path and JSON
  writeProjectFileAtomically->>NodeFilesystem: write and sync temporary file
  writeProjectFileAtomically->>NodeFilesystem: preserve existing project as backup
  writeProjectFileAtomically->>NodeFilesystem: rename temporary file into place
  writeProjectFileAtomically->>NodeFilesystem: sync parent directory
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: making project saves atomic.
Description check ✅ Passed The description covers purpose, motivation, change type, issues, testing, and checklist; only non-critical optional sections are omitted.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/project-atomic-save

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@meiiie meiiie marked this pull request as ready for review July 11, 2026 03:40

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
electron/ipc/register/project.ts (1)

428-445: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Orphaned .bak left behind on rename.

The atomic writer now creates a <project>.bak alongside each saved project. In the "rename" branch the old project file (Line 429) and its thumbnail (Line 434) are removed, but the old project's backup at getProjectBackupPath(activeProjectPath) is not, so it will accumulate as an orphan in the projects directory. Suggest cleaning it up alongside the thumbnail.

🧹 Proposed cleanup
             await fs.rm(getProjectThumbnailPath(activeProjectPath), { force: true }).catch(() => undefined)
+            await fs.rm(getProjectBackupPath(activeProjectPath), { force: true }).catch(() => undefined)

Note: getProjectBackupPath is exported from ../project/atomicSave and would need importing here.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@electron/ipc/register/project.ts` around lines 428 - 445, Clean up the old
project's atomic-save backup during the rename flow in the named-save branch.
Import the exported getProjectBackupPath helper from ../project/atomicSave, then
remove getProjectBackupPath(activeProjectPath) alongside the thumbnail cleanup
after unlinking the old project, tolerating a missing backup file.
🧹 Nitpick comments (3)
electron/ipc/project/atomicSave.test.ts (3)

36-44: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider asserting no temporary artifacts in the stale-backup test.

The other tests call expectNoTemporaryArtifacts() to verify cleanup, but this one omits the check. Adding it keeps the assertion contract consistent across all tests and guards against leftover .tmp files in the stale-backup cleanup path.

🧪 Suggested addition
 	await expect(fs.access(getProjectBackupPath(projectPath))).rejects.toMatchObject({
 		code: "ENOENT",
 	});
+	await expectNoTemporaryArtifacts();
 });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@electron/ipc/project/atomicSave.test.ts` around lines 36 - 44, Add an
expectNoTemporaryArtifacts() assertion to the stale-backup test after
writeProjectFileAtomically completes and the stale backup assertion, matching
the cleanup checks used by the other tests.

78-91: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Concurrent serialization test assumes deterministic order.

The test asserts the final content is {"revision":3} and backup is {"revision":2}, which requires revision 2 to commit before revision 3. While the queue serialization in writeProjectFileAtomically preserves call order, Promise.all does not guarantee the order in which the two writeProjectFileAtomically calls begin executing. The first call to reach pendingWrites.set wins the queue position. In practice, since both calls are synchronous up to the await currentWrite, the order is deterministic, but this is subtle. Consider adding a brief comment explaining why the order is guaranteed to prevent future confusion.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@electron/ipc/project/atomicSave.test.ts` around lines 78 - 91, Add a brief
comment in the “serializes overlapping writes to the same project” test
explaining that each write reaches the queue synchronously before its first
await, so Promise.all preserves invocation order and revision 2 commits before
revision 3. Keep the existing assertions unchanged.

57-76: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert no temporary artifacts remain after the failed write.

The finally block in commitProjectFile is supposed to clean up both temp files even when the commit fails, but this test only checks expectNoTemporaryArtifacts() after the subsequent successful retry (line 75). Adding the assertion right after the rejected write (after line 65) would verify the failure-path cleanup independently.

🧪 Suggested addition
 	await expect(fs.readFile(projectPath, "utf-8")).resolves.toBe('{"version":1,"name":"old"}');
+	await expectNoTemporaryArtifacts();
 
 	await fs.rm(getProjectBackupPath(projectPath), { recursive: true });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@electron/ipc/project/atomicSave.test.ts` around lines 57 - 76, Update the
test case “keeps the active generation unchanged when backup commit fails” to
call expectNoTemporaryArtifacts() immediately after the rejected write
assertion, before removing the backup directory or retrying. Keep the existing
final cleanup assertion as well.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@electron/ipc/project/atomicSave.ts`:
- Around line 42-54: Update writeSyncedTemporaryFile to preserve exact POSIX
permissions: after opening the file and when mode is defined, apply handle-based
chmod/fchmod with that mode before syncing, while retaining the existing write
and guaranteed close flow.

---

Outside diff comments:
In `@electron/ipc/register/project.ts`:
- Around line 428-445: Clean up the old project's atomic-save backup during the
rename flow in the named-save branch. Import the exported getProjectBackupPath
helper from ../project/atomicSave, then remove
getProjectBackupPath(activeProjectPath) alongside the thumbnail cleanup after
unlinking the old project, tolerating a missing backup file.

---

Nitpick comments:
In `@electron/ipc/project/atomicSave.test.ts`:
- Around line 36-44: Add an expectNoTemporaryArtifacts() assertion to the
stale-backup test after writeProjectFileAtomically completes and the stale
backup assertion, matching the cleanup checks used by the other tests.
- Around line 78-91: Add a brief comment in the “serializes overlapping writes
to the same project” test explaining that each write reaches the queue
synchronously before its first await, so Promise.all preserves invocation order
and revision 2 commits before revision 3. Keep the existing assertions
unchanged.
- Around line 57-76: Update the test case “keeps the active generation unchanged
when backup commit fails” to call expectNoTemporaryArtifacts() immediately after
the rejected write assertion, before removing the backup directory or retrying.
Keep the existing final cleanup assertion as well.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: e5574d80-879e-4bc5-9a66-b81da45a0b29

📥 Commits

Reviewing files that changed from the base of the PR and between 3ae326a and cf4416a.

📒 Files selected for processing (3)
  • electron/ipc/project/atomicSave.test.ts
  • electron/ipc/project/atomicSave.ts
  • electron/ipc/register/project.ts

Comment thread electron/ipc/project/atomicSave.ts
@meiiie meiiie merged commit 1ab9d68 into main Jul 11, 2026
4 checks passed
@meiiie meiiie deleted the fix/project-atomic-save branch July 11, 2026 03:48
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.

1 participant