Skip to content

Commit 9182256

Browse files
committed
feat(download-lock): align stale timeout with npm and use in dlxBinary
- Reduce default staleTimeout from 300s to 10s (align with npm's 5-10s range) - Use downloadWithLock in dlxBinary.downloadBinary() for concurrent protection - Prevents corruption when multiple processes download same binary - Preserves checksum verification after download completes - Sets 2-minute lockTimeout for large binary downloads Benefits: - Aligns with npm's battle-tested npx locking strategy - Prevents TAR_ENTRY_ERROR, ENOTEMPTY, MODULE_NOT_FOUND issues - Faster stale lock detection (10s vs 5 minutes) - Better concurrent download handling across processes
1 parent c8d0068 commit 9182256

2 files changed

Lines changed: 29 additions & 49 deletions

File tree

src/dlx-binary.ts

Lines changed: 26 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ import path from 'node:path'
77

88
import { WIN32 } from '#constants/platform'
99

10+
import { downloadWithLock } from './download-lock'
1011
import { isDir, readJson, safeDelete } from './fs'
11-
import { httpRequest } from './http-request'
1212
import { isObjectObject } from './objects'
1313
import { normalizePath } from './path'
1414
import { getSocketDlxDir } from './paths'
@@ -88,63 +88,42 @@ async function isCacheValid(
8888
}
8989

9090
/**
91-
* Download a file from a URL with integrity checking.
91+
* Download a file from a URL with integrity checking and concurrent download protection.
92+
* Uses downloadWithLock to prevent multiple processes from downloading the same binary simultaneously.
9293
*/
9394
async function downloadBinary(
9495
url: string,
9596
destPath: string,
9697
checksum?: string | undefined,
9798
): Promise<string> {
98-
const response = await httpRequest(url)
99-
if (!response.ok) {
99+
// Use downloadWithLock to handle concurrent download protection.
100+
// This prevents corruption when multiple processes try to download the same binary.
101+
await downloadWithLock(url, destPath, {
102+
staleTimeout: 10_000, // Align with npm's npx locking strategy
103+
lockTimeout: 120_000, // Allow up to 2 minutes for large binary downloads
104+
})
105+
106+
// Compute checksum of downloaded file.
107+
const fileBuffer = await fs.readFile(destPath)
108+
const hasher = createHash('sha256')
109+
hasher.update(fileBuffer)
110+
const actualChecksum = hasher.digest('hex')
111+
112+
// Verify checksum if provided.
113+
if (checksum && actualChecksum !== checksum) {
114+
// Clean up invalid file.
115+
await safeDelete(destPath)
100116
throw new Error(
101-
`Failed to download binary: ${response.status} ${response.statusText}`,
117+
`Checksum mismatch: expected ${checksum}, got ${actualChecksum}`,
102118
)
103119
}
104120

105-
// Create a temporary file first.
106-
const tempPath = `${destPath}.download`
107-
const hasher = createHash('sha256')
108-
109-
try {
110-
// Ensure directory exists.
111-
await fs.mkdir(path.dirname(destPath), { recursive: true })
112-
113-
// Get the response as a buffer and compute hash.
114-
const buffer = response.body
115-
116-
// Compute hash.
117-
hasher.update(buffer)
118-
const actualChecksum = hasher.digest('hex')
119-
120-
// Verify checksum if provided.
121-
if (checksum && actualChecksum !== checksum) {
122-
throw new Error(
123-
`Checksum mismatch: expected ${checksum}, got ${actualChecksum}`,
124-
)
125-
}
126-
127-
// Write to temp file.
128-
await fs.writeFile(tempPath, buffer)
129-
130-
// Make executable on POSIX systems.
131-
if (!WIN32) {
132-
await fs.chmod(tempPath, 0o755)
133-
}
134-
135-
// Move temp file to final location.
136-
await fs.rename(tempPath, destPath)
137-
138-
return actualChecksum
139-
} catch (e) {
140-
// Clean up temp file on error.
141-
try {
142-
await safeDelete(tempPath)
143-
} catch {
144-
// Ignore cleanup errors.
145-
}
146-
throw e
121+
// Make executable on POSIX systems.
122+
if (!WIN32) {
123+
await fs.chmod(destPath, 0o755)
147124
}
125+
126+
return actualChecksum
148127
}
149128

150129
/**

src/download-lock.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ export interface DownloadWithLockOptions extends HttpDownloadOptions {
3030
pollInterval?: number | undefined
3131
/**
3232
* Maximum age of a lock before it's considered stale in milliseconds.
33-
* @default 300000 (5 minutes)
33+
* Aligned with npm's npx locking strategy (5-10 seconds).
34+
* @default 10000 (10 seconds)
3435
*/
3536
staleTimeout?: number | undefined
3637
}
@@ -191,7 +192,7 @@ export async function downloadWithLock(
191192
lockTimeout = 60_000,
192193
locksDir,
193194
pollInterval = 1000,
194-
staleTimeout = 300_000,
195+
staleTimeout = 10_000, // Aligned with npm's npx locking (5-10s range)
195196
...downloadOptions
196197
} = { __proto__: null, ...options } as DownloadWithLockOptions
197198

0 commit comments

Comments
 (0)