Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@
- Safe Deletion: Use `safeDelete()` from `@socketsecurity/lib/fs` (NEVER `fs.rm/rmSync` or `rm -rf`)
- HTTP Requests: NEVER use `fetch()` — use `httpJson`/`httpText`/`httpRequest` from `@socketsecurity/lib/http-request`
- File existence: ALWAYS `existsSync` from `node:fs`. NEVER `fs.access`, `fs.stat`-for-existence, or an async `fileExists` wrapper. Import form: `import { existsSync, promises as fs } from 'node:fs'`.
- `Promise.race` / `Promise.any`: NEVER pass a long-lived promise (interrupt signal, pool member) into a race inside a loop. Each call re-attaches `.then` handlers to every arm; handlers accumulate on surviving promises until they settle. For concurrency limiters, use a single-waiter "slot available" signal (resolved by each task's `.then`) instead of re-racing `executing[]`. See nodejs/node#17469 and `@watchable/unpromise`. Race with two fresh arms (e.g. one-shot `withTimeout`) is safe.

---

Expand Down Expand Up @@ -149,6 +150,7 @@ Core infrastructure library for Socket.dev security tools.
- **Null-prototype objects**: `{ __proto__: null, ...props }`
- **Exports**: Named only. `export default` FORBIDDEN (breaks dual CJS/ESM). Enforced by oxlint `no-default-export` + build + CI validation.
- **Function order**: Files with 3+ exports require alphabetical ordering — private first (alphabetical), then exported (alphabetical). Constants/types before functions.
- **Promise.race in loops**: NEVER re-race the same pool across iterations. Each race attaches fresh `.then` handlers to every arm — a promise that survives N iterations accumulates N handler sets ([nodejs/node#17469](https://github.com/nodejs/node/issues/17469)). Fresh-both-arms races (e.g. `Promise.race([work(), timeoutPromise()])` where both are per-call) are fine. For concurrency limiters, use a single-waiter signal (`promiseWithResolvers` swapped per iteration), never `Promise.race(pool)` over a persistent pool.

### Package Exports

Expand Down
48 changes: 36 additions & 12 deletions scripts/claude.mts
Original file line number Diff line number Diff line change
Expand Up @@ -1906,23 +1906,47 @@ async function executeParallel(tasks, workers = 3) {
return results
}

// Parallel execution with worker limit
// Parallel execution with worker limit.
// A single-waiter "slot available" signal wakes this loop whenever any
// in-flight task settles, instead of re-racing the whole executing[] array
// on every iteration (that stacks .then handlers on still-pending promises
// — see https://github.com/nodejs/node/issues/17469).
log.substep(`🚀 Executing ${tasks.length} tasks with ${workers} workers`)
const results = []
const executing = []

for (const task of tasks) {
const promise = task().then(result => {
executing.splice(executing.indexOf(promise), 1)
return result
let inFlight = 0
let slotWaiter = null

const releaseSlot = () => {
inFlight--
if (slotWaiter) {
const resolve = slotWaiter
slotWaiter = null
resolve()
}
}
const waitForSlot = () => {
if (inFlight < workers) {
return Promise.resolve()
}
return new Promise(resolve => {
slotWaiter = resolve
})
}

for (const task of tasks) {
await waitForSlot()
inFlight++
const promise = task().then(
result => {
releaseSlot()
return result
},
err => {
releaseSlot()
throw err
},
)
results.push(promise)
executing.push(promise)

if (executing.length >= workers) {
await Promise.race(executing)
}
}

return Promise.all(results)
Expand Down