File size: 1,761 Bytes
4e23b01
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// src/rename-replace.ts
//
// `fs.rename(src, dst)` with Windows replace-open-destination semantics.
//
// Windows, unlike POSIX, refuses to rename over a destination that ANY
// process still holds open (no delete sharing by default): libuv returns
// EPERM. Transient openers — co-process readers doing a split-second
// readFile/stat, antivirus, file indexers — come and go within single-digit
// milliseconds, so on Windows the caller-side facility retries EPERM with
// jitter before giving up. POSIX renames over open files directly and never
// takes the retry path.

import fs from 'node:fs/promises';

const sleep = (ms: number): Promise<void> => new Promise((r) => setTimeout(r, ms));

export interface RenameReplaceOptions {
  /** Max EPERM retries before the error propagates (Windows only). */
  retries?: number;
  /** Base delay between retries in ms (a like-sized random jitter is added). */
  baseDelayMs?: number;
}

export async function retryEperm<T>(op: () => Promise<T>, opts: RenameReplaceOptions = {}): Promise<T> {
  const retries = opts.retries ?? 100;
  const base = opts.baseDelayMs ?? 20;
  for (let attempt = 0; ; attempt++) {
    try {
      return await op();
    } catch (e) {
      if ((e as NodeJS.ErrnoException).code !== 'EPERM' || attempt >= retries) throw e;
      await sleep(base + Math.floor(Math.random() * (base + 10)));
    }
  }
}

export async function withWindowsEpermRetry<T>(op: () => Promise<T>, opts: RenameReplaceOptions = {}): Promise<T> {
  if (process.platform !== 'win32') return op();
  return retryEperm(op, opts);
}

export async function renameReplace(src: string, dst: string, opts: RenameReplaceOptions = {}): Promise<void> {
  return withWindowsEpermRetry(() => fs.rename(src, dst), opts);
}