From 30df5a48af7bc263aa6d09ba19d44a3339338b07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Wed, 28 Oct 2026 09:12:54 +0000 Subject: [PATCH] fix(studio-server): undo and redo on Windows restore a file something holds open (#6628) * fix(studio-server): undo and redo on Windows restore a file something holds open * fix(studio-server): a restore waits for the preview's reads to close before replacing the file * fix(studio-server): a restore checks the project folder after the preview's reads close --- packages/core/src/renameWithRetry.ts | 4 +- .../studio-server/src/helpers/servedFiles.ts | 32 ++++++ .../src/history/blobStore.test.ts | 106 +++++++++++++++++- .../studio-server/src/history/blobStore.ts | 57 ++++++++-- packages/studio-server/src/history/index.ts | 1 + packages/studio-server/src/routes/preview.ts | 3 +- 6 files changed, 189 insertions(+), 14 deletions(-) create mode 100644 packages/studio-server/src/helpers/servedFiles.ts 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