Skip to content

Commit 6854e73

Browse files
author
Eric Wheeler
committed
fix: improve safeWriteJson locking mechanism
- Use file path itself for locking instead of separate lock file - Improve error handling and clarity of code - Enhance cleanup of temporary files Signed-off-by: Eric Wheeler <[email protected]>
1 parent a1b1a09 commit 6854e73

File tree

1 file changed

+14
-11
lines changed

1 file changed

+14
-11
lines changed

src/utils/safeWriteJson.ts

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,11 @@ import Stringer from "stream-json/Stringer"
1919

2020
async function safeWriteJson(filePath: string, data: any): Promise<void> {
2121
const absoluteFilePath = path.resolve(filePath)
22-
const lockPath = `${absoluteFilePath}.lock`
2322
let releaseLock = async () => {} // Initialized to a no-op
2423

2524
// Acquire the lock before any file operations
2625
try {
27-
releaseLock = await lockfile.lock(lockPath, {
26+
releaseLock = await lockfile.lock(absoluteFilePath, {
2827
stale: 31000, // Stale after 31 seconds
2928
update: 10000, // Update mtime every 10 seconds to prevent staleness if operation is long
3029
retries: {
@@ -34,18 +33,18 @@ async function safeWriteJson(filePath: string, data: any): Promise<void> {
3433
minTimeout: 100, // Minimum time to wait before the first retry (in ms)
3534
maxTimeout: 1000, // Maximum time to wait for any single retry (in ms)
3635
},
37-
realpath: false, // Skip realpath check as we've already resolved absoluteFilePath
3836
onCompromised: (err) => {
39-
console.error(`Lock at ${lockPath} was compromised:`, err)
37+
console.error(`Lock at ${absoluteFilePath} was compromised:`, err)
4038
throw err
4139
},
4240
})
4341
} catch (lockError) {
4442
// If lock acquisition fails, we throw immediately.
4543
// The releaseLock remains a no-op, so the finally block in the main file operations
4644
// try-catch-finally won't try to release an unacquired lock if this path is taken.
47-
console.error(`Failed to acquire lock for ${lockPath}:`, lockError)
48-
throw lockError // Propagate the lock acquisition error
45+
console.error(`Failed to acquire lock for ${absoluteFilePath}:`, lockError)
46+
// Propagate the lock acquisition error
47+
throw lockError
4948
}
5049

5150
// Variables to hold the actual paths of temp files if they are created.
@@ -63,7 +62,8 @@ async function safeWriteJson(filePath: string, data: any): Promise<void> {
6362

6463
// Step 2: Check if the target file exists. If so, rename it to a backup path.
6564
try {
66-
await fs.access(absoluteFilePath) // Check for target file existence
65+
// Check for target file existence
66+
await fs.access(absoluteFilePath)
6767
// Target exists, create a backup path and rename.
6868
actualTempBackupFilePath = path.join(
6969
path.dirname(absoluteFilePath),
@@ -85,13 +85,15 @@ async function safeWriteJson(filePath: string, data: any): Promise<void> {
8585

8686
// If we reach here, the new file is successfully in place.
8787
// The original actualTempNewFilePath is now the main file, so we shouldn't try to clean it up as "temp".
88-
actualTempNewFilePath = null // Mark as "used" or "committed"
88+
// Mark as "used" or "committed"
89+
actualTempNewFilePath = null
8990

9091
// Step 4: If a backup was created, attempt to delete it.
9192
if (actualTempBackupFilePath) {
9293
try {
9394
await fs.unlink(actualTempBackupFilePath)
94-
actualTempBackupFilePath = null // Mark backup as handled
95+
// Mark backup as handled
96+
actualTempBackupFilePath = null
9597
} catch (unlinkBackupError) {
9698
// Log this error, but do not re-throw. The main operation was successful.
9799
// actualTempBackupFilePath remains set, indicating an orphaned backup.
@@ -111,7 +113,8 @@ async function safeWriteJson(filePath: string, data: any): Promise<void> {
111113
if (backupFileToRollbackOrCleanupWithinCatch) {
112114
try {
113115
await fs.rename(backupFileToRollbackOrCleanupWithinCatch, absoluteFilePath)
114-
actualTempBackupFilePath = null // Mark as handled, prevent later unlink of this path
116+
// Mark as handled, prevent later unlink of this path
117+
actualTempBackupFilePath = null
115118
} catch (rollbackError) {
116119
// actualTempBackupFilePath (outer scope) remains pointing to backupFileToRollbackOrCleanupWithinCatch
117120
console.error(
@@ -153,7 +156,7 @@ async function safeWriteJson(filePath: string, data: any): Promise<void> {
153156
await releaseLock()
154157
} catch (unlockError) {
155158
// Do not re-throw here, as the originalError from the try/catch (if any) is more important.
156-
console.error(`Failed to release lock for ${lockPath}:`, unlockError)
159+
console.error(`Failed to release lock for ${absoluteFilePath}:`, unlockError)
157160
}
158161
}
159162
}

0 commit comments

Comments
 (0)