Skip to content

Commit b5c177b

Browse files
author
“graciegould”
committed
fix: deduplicate concurrent downloads in resolveModelFile
When multiple concurrent calls to resolveModelFile target the same model, each call independently starts a download via ipull. Multiple ipull instances writing to the same .ipull temp file causes orphaned FileHandle objects when the first download completes and renames the file. On Node 22+, garbage-collected FileHandles are a fatal error. Add a module-level Map to track in-flight downloads by entrypoint file path. When a concurrent call detects an existing download for the same file, it cancels its own downloader and awaits the existing promise instead of starting a duplicate download. Closes #569
1 parent 5a44506 commit b5c177b

1 file changed

Lines changed: 33 additions & 10 deletions

File tree

src/utils/resolveModelFile.ts

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,13 @@ import {genericFilePartNumber} from "./parseModelUri.js";
1212
import {isFilePartText} from "./parseModelFileName.js";
1313
import {pushAll} from "./pushAll.js";
1414

15+
/**
16+
* In-flight download promises keyed by resolved file path.
17+
* Prevents concurrent `resolveModelFile` calls from starting duplicate downloads
18+
* to the same destination, which causes FileHandle GC errors on Node 22+
19+
* when two ipull instances write to the same `.ipull` temp file.
20+
*/
21+
const inFlightDownloads = new Map<string, Promise<string>>();
1522

1623
export type ResolveModelFileOptions = {
1724
/**
@@ -270,19 +277,35 @@ export async function resolveModelFile(
270277
}
271278
}
272279

273-
if (resolvedCli)
274-
console.info(`Downloading to ${chalk.yellow(getReadablePath(resolvedDirectory))}${
275-
downloader.splitBinaryParts != null
276-
? chalk.gray(` (combining ${downloader.splitBinaryParts} parts into a single file)`)
277-
: ""
278-
}`);
280+
const downloadKey = downloader.entrypointFilePath;
281+
const existingDownload = inFlightDownloads.get(downloadKey);
282+
if (existingDownload != null) {
283+
await downloader.cancel({deleteTempFile: false});
284+
return existingDownload;
285+
}
286+
287+
const downloadPromise = (async () => {
288+
try {
289+
if (resolvedCli)
290+
console.info(`Downloading to ${chalk.yellow(getReadablePath(resolvedDirectory))}${
291+
downloader.splitBinaryParts != null
292+
? chalk.gray(` (combining ${downloader.splitBinaryParts} parts into a single file)`)
293+
: ""
294+
}`);
295+
296+
await downloader.download({signal});
279297

280-
await downloader.download({signal});
298+
if (resolvedCli)
299+
console.info(`Downloaded to ${chalk.yellow(getReadablePath(downloader.entrypointFilePath))}`);
281300

282-
if (resolvedCli)
283-
console.info(`Downloaded to ${chalk.yellow(getReadablePath(downloader.entrypointFilePath))}`);
301+
return downloader.entrypointFilePath;
302+
} finally {
303+
inFlightDownloads.delete(downloadKey);
304+
}
305+
})();
284306

285-
return downloader.entrypointFilePath;
307+
inFlightDownloads.set(downloadKey, downloadPromise);
308+
return downloadPromise;
286309
}
287310

288311
async function findMatchingFilesInDirectory(dirPath: string, fileNames: (string | `${string}${typeof genericFilePartNumber}${string}`)[]) {

0 commit comments

Comments
 (0)