( filePath: string, content: string | Uint8Array, _syncOverride?: (fd: number) => Promise<void>, )
| 147 | * fault injection. Production callers must never supply this. |
| 148 | */ |
| 149 | export async function atomicWrite( |
| 150 | filePath: string, |
| 151 | content: string | Uint8Array, |
| 152 | _syncOverride?: (fd: number) => Promise<void>, |
| 153 | ): Promise<void> { |
| 154 | const hex = randomBytes(4).toString('hex'); |
| 155 | const tmpPath = `${filePath}.tmp.${process.pid}.${hex}`; |
| 156 | let renamed = false; |
| 157 | try { |
| 158 | const fh = await open(tmpPath, 'w'); |
| 159 | try { |
| 160 | await fh.writeFile(content); |
| 161 | await (_syncOverride ?? syncFd)(fh.fd); |
| 162 | } finally { |
| 163 | await fh.close(); |
| 164 | } |
| 165 | // Windows `fs.rename` maps to MoveFileEx and fails with EPERM if |
| 166 | // the target is held by another handle. Pre-unlinking |
| 167 | // before the rename turns this into the POSIX-style "replace" case. |
| 168 | if (process.platform === 'win32') { |
| 169 | try { |
| 170 | await unlink(filePath); |
| 171 | } catch (error) { |
| 172 | const code = (error as NodeJS.ErrnoException).code; |
| 173 | if (code !== 'ENOENT') throw error; |
| 174 | } |
| 175 | } |
| 176 | await rename(tmpPath, filePath); |
| 177 | renamed = true; |
| 178 | } finally { |
| 179 | if (!renamed) { |
| 180 | try { |
| 181 | await unlink(tmpPath); |
| 182 | } catch { |
| 183 | /* ignore — file may not exist if open itself failed */ |
| 184 | } |
| 185 | } |
| 186 | } |
| 187 | } |
no test coverage detected