diff --git a/packages/core/src/renameWithRetry.ts b/packages/core/src/renameWithRetry.ts index f0d62e1301..a5ae09da4b 100644 --- a/packages/core/src/renameWithRetry.ts +++ b/packages/core/src/renameWithRetry.ts @@ -13,14 +13,14 @@ interface Options { copyFallback?: boolean; } -function isRetryable(error: unknown, platform: NodeJS.Platform): boolean { +export function isWindowsHold(error: unknown, platform = process.platform): boolean { if (platform !== "win32") return false; return RETRYABLE.has((error as NodeJS.ErrnoException | undefined)?.code ?? ""); } /** What a refused rename does next: wait and try again, fall back to a copy, or give up with the error. */ function nextStep(error: unknown, attempt: number, options: Options, platform: NodeJS.Platform) { - if (!isRetryable(error, platform)) return "throw"; + if (!isWindowsHold(error, platform)) return "throw"; if (attempt < BACKOFF_MS.length) return "wait"; return options.copyFallback ? "copy" : "throw"; } diff --git a/packages/studio-server/src/helpers/servedFiles.ts b/packages/studio-server/src/helpers/servedFiles.ts new file mode 100644 index 0000000000..90db7f86c0 --- /dev/null +++ b/packages/studio-server/src/helpers/servedFiles.ts @@ -0,0 +1,32 @@ +import type { ReadStream } from "node:fs"; +import { resolve } from "node:path"; + +const serving = new Map>(); +const keyOf = (path: string) => + process.platform === "win32" ? resolve(path).toLowerCase() : resolve(path); + +export function serveWhileOpen(path: string, stream: ReadStream): ReadStream { + const key = keyOf(path); + const reads = serving.get(key) ?? new Set(); + serving.set(key, reads.add(stream)); + stream.once("close", () => { + reads.delete(stream); + if (reads.size === 0 && serving.get(key) === reads) serving.delete(key); + }); + return stream; +} + +// Windows refuses to replace a file while a read of it is open, and a loaded