Skip to content

Commit 1c9e4b8

Browse files
committed
fix(json): add retry logic for Windows EPERM errors during file writes
Windows can have transient file locking issues in temp directories, causing EPERM errors when writing files. This adds exponential backoff retry logic to handle these transient failures. - Add retryWrite() helper with exponential backoff (10ms, 20ms, 40ms) - Retry on EPERM and EBUSY errors up to 3 times - Fixes flaky test: 'should preserve line endings' on Windows CI
1 parent 7a2b6d6 commit 1c9e4b8

File tree

1 file changed

+40
-3
lines changed

1 file changed

+40
-3
lines changed

src/json/edit.ts

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,44 @@ function getFs() {
4141
return _fs as typeof import('node:fs')
4242
}
4343

44+
/**
45+
* Retry a file write operation with exponential backoff on Windows EPERM errors.
46+
* Windows can have transient file locking issues with temp directories.
47+
* @private
48+
*/
49+
async function retryWrite(
50+
filepath: string,
51+
content: string,
52+
retries = 3,
53+
baseDelay = 10,
54+
): Promise<void> {
55+
const { promises: fsPromises } = getFs()
56+
57+
for (let attempt = 0; attempt <= retries; attempt++) {
58+
try {
59+
// eslint-disable-next-line no-await-in-loop
60+
await fsPromises.writeFile(filepath, content)
61+
return
62+
} catch (err) {
63+
const isLastAttempt = attempt === retries
64+
const isEperm =
65+
err instanceof Error &&
66+
'code' in err &&
67+
(err.code === 'EPERM' || err.code === 'EBUSY')
68+
69+
// Only retry on Windows EPERM/EBUSY errors, and not on the last attempt
70+
if (!isEperm || isLastAttempt) {
71+
throw err
72+
}
73+
74+
// Exponential backoff: 10ms, 20ms, 40ms
75+
const delay = baseDelay * 2 ** attempt
76+
// eslint-disable-next-line no-await-in-loop
77+
await new Promise(resolve => setTimeout(resolve, delay))
78+
}
79+
}
80+
}
81+
4482
/**
4583
* Parse JSON content and extract formatting metadata.
4684
* @private
@@ -216,9 +254,8 @@ export function getEditableJsonClass<
216254
// Generate file content
217255
const fileContent = stringifyWithFormatting(sortedContent, formatting)
218256

219-
// Save to disk
220-
const { promises: fsPromises } = getFs()
221-
await fsPromises.writeFile(this.filename, fileContent)
257+
// Save to disk with retry logic for Windows file locking issues
258+
await retryWrite(this.filename, fileContent)
222259
this._readFileContent = fileContent
223260
this._readFileJson = parseJson(fileContent)
224261
return true

0 commit comments

Comments
 (0)