Skip to content

Commit 2a8e693

Browse files
committed
feat(dlx-package): add process-lock for concurrent installation protection
Wrap ensurePackageInstalled() with processLock.withLock() to prevent concurrent installations from corrupting the cache. This aligns Socket's dlx implementation with npm npx's concurrency.lock strategy. Key changes: - Lock file created at ~/.socket/_dlx/<hash>/.lock - Uses 5s stale timeout and 2s periodic touching (aligned with npm npx) - Double-check pattern: verify installation after acquiring lock - Prevents race conditions when multiple processes install same package This completes 100% alignment with npm's npx locking mechanism, closing the final gap identified in lock-and-dlx-comparison.md. All 30 existing tests pass with the new locking behavior.
1 parent b1f86c9 commit 2a8e693

1 file changed

Lines changed: 44 additions & 21 deletions

File tree

src/dlx-package.ts

Lines changed: 44 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@
99
* - Each unique spec gets its own directory: ~/.socket/_dlx/<hash>/
1010
* - Allows caching multiple versions of the same package
1111
*
12+
* Concurrency protection:
13+
* - Uses process-lock to prevent concurrent installation corruption
14+
* - Lock file created at ~/.socket/_dlx/<hash>/.lock
15+
* - Aligned with npm npx's concurrency.lock strategy (5s stale, 2s touching)
16+
* - Prevents multiple processes from corrupting the same package installation
17+
*
1218
* Version range handling:
1319
* - Exact versions (1.0.0) use cache if available
1420
* - Range versions (^1.0.0, ~1.0.0) auto-force to get latest within range
@@ -34,6 +40,7 @@ import { getPacoteCachePath } from './constants/packages'
3440
import { readJsonSync } from './fs'
3541
import { normalizePath } from './path'
3642
import { getSocketDlxDir } from './paths'
43+
import { processLock } from './process-lock'
3744
import type { SpawnExtra, SpawnOptions } from './spawn'
3845
import { spawn } from './spawn'
3946

@@ -128,6 +135,7 @@ function parsePackageSpec(spec: string): {
128135
/**
129136
* Install package to ~/.socket/_dlx/<hash>/ if not already installed.
130137
* Uses pacote for installation (no npm CLI required).
138+
* Protected by process lock to prevent concurrent installation corruption.
131139
*/
132140
async function ensurePackageInstalled(
133141
packageSpec: string,
@@ -140,27 +148,42 @@ async function ensurePackageInstalled(
140148
path.join(packageDir, 'node_modules', packageName),
141149
)
142150

143-
// Check if already installed (unless force).
144-
if (!force && existsSync(installedDir)) {
145-
// Verify package.json exists.
146-
const pkgJsonPath = path.join(installedDir, 'package.json')
147-
if (existsSync(pkgJsonPath)) {
148-
return { installed: false, packageDir }
149-
}
150-
}
151-
152-
// Ensure package directory exists.
153-
await fs.mkdir(packageDir, { recursive: true })
154-
155-
// Use pacote to extract the package.
156-
// Pacote leverages npm cache when available but doesn't require npm CLI.
157-
const pacoteCachePath = getPacoteCachePath()
158-
await pacote.extract(packageSpec, installedDir, {
159-
// Use consistent pacote cache path (respects npm cache locations when available).
160-
cache: pacoteCachePath || path.join(packageDir, '.cache'),
161-
})
162-
163-
return { installed: true, packageDir }
151+
// Use process lock to prevent concurrent installations.
152+
// Similar to npm npx's concurrency.lock approach.
153+
const lockPath = path.join(packageDir, '.lock')
154+
155+
return await processLock.withLock(
156+
lockPath,
157+
async () => {
158+
// Double-check if already installed (unless force).
159+
// Another process may have installed while waiting for lock.
160+
if (!force && existsSync(installedDir)) {
161+
// Verify package.json exists.
162+
const pkgJsonPath = path.join(installedDir, 'package.json')
163+
if (existsSync(pkgJsonPath)) {
164+
return { installed: false, packageDir }
165+
}
166+
}
167+
168+
// Ensure package directory exists.
169+
await fs.mkdir(packageDir, { recursive: true })
170+
171+
// Use pacote to extract the package.
172+
// Pacote leverages npm cache when available but doesn't require npm CLI.
173+
const pacoteCachePath = getPacoteCachePath()
174+
await pacote.extract(packageSpec, installedDir, {
175+
// Use consistent pacote cache path (respects npm cache locations when available).
176+
cache: pacoteCachePath || path.join(packageDir, '.cache'),
177+
})
178+
179+
return { installed: true, packageDir }
180+
},
181+
{
182+
// Align with npm npx locking strategy.
183+
staleMs: 5000,
184+
touchIntervalMs: 2000,
185+
},
186+
)
164187
}
165188

166189
/**

0 commit comments

Comments
 (0)