1
import { realpath } from "node:fs/promises";2
import { resolve } from "node:path";4
const fileMutationQueues = new Map<string, Promise<void>>();5
let registrationQueue = Promise.resolve();7
function isMissingPathError(error: unknown): boolean {8
return (9
typeof error === "object" &&10
error !== null &&11
"code" in error &&12
(error.code === "ENOENT" || error.code === "ENOTDIR")13
);14
}16
async function getMutationQueueKey(filePath: string): Promise<string> {17
const resolvedPath = resolve(filePath);18
try {19
return await realpath(resolvedPath);20
} catch (error) {21
if (isMissingPathError(error)) {22
return resolvedPath;23
}24
throw error;25
}26
}28
/**29
* Serialize file mutation operations targeting the same file.30
* Operations for different files still run in parallel.31
*/32
export async function withFileMutationQueue<T>(filePath: string, fn: () => Promise<T>): Promise<T> {33
const registration = registrationQueue.then(async () => {34
const key = await getMutationQueueKey(filePath);35
const currentQueue = fileMutationQueues.get(key) ?? Promise.resolve();37
let releaseNext!: () => void;38
const nextQueue = new Promise<void>((resolveQueue) => {39
releaseNext = resolveQueue;40
});41
const chainedQueue = currentQueue.then(() => nextQueue);42
fileMutationQueues.set(key, chainedQueue);44
return { key, currentQueue, chainedQueue, releaseNext };45
});46
registrationQueue = registration.then(47
() => undefined,48
() => undefined,49
);51
const { key, currentQueue, chainedQueue, releaseNext } = await registration;52
await currentQueue;53
try {54
return await fn();55
} finally {56
releaseNext();57
if (fileMutationQueues.get(key) === chainedQueue) {58
fileMutationQueues.delete(key);59
}60
}61
}