Files
Benjamin Taylor 2a1a141389 perf(release): publish canary packages with bounded concurrency
After the npx fix each publish is ~4.7s and almost entirely a registry
round-trip, so a 26-package scope=all canary still spent ~125s waiting
serially. Publish 4 at a time (CANARY_PUBLISH_CONCURRENCY=1 restores
serial for debugging).

This weakens no ordering invariant. prerelease.ts's own header already
documents that the cross-scope graph has cycles (runtime ->
channels-intelligence, channels-core -> core), so no serial order avoided
publishing a package before the same-run version it pins.

Per-package output is captured and replayed as one block rather than
inherited, since a pool would otherwise interleave several npm publishes
line-by-line — and that log is the only forensic record when a canary
half-publishes. Every package is attempted even if others fail, so one
report names all of them; main() now exits non-zero on failure rather
than letting an unhandled rejection pass the step.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 19:53:05 -05:00

61 lines
2.2 KiB
TypeScript

/**
* Bounded-concurrency task runner for the publish loop.
*
* Publishing N packages serially costs N x (pack + registry round-trip). After
* the npx fix (see npm-cli.ts) that is ~4.7s per package, so a 26-package
* `scope=all` canary still spent ~125s almost entirely waiting on the network.
*
* Ordering is deliberately NOT preserved as a correctness property: prerelease.ts
* documents that the cross-scope dependency graph has cycles (runtime ->
* channels-intelligence, channels-core -> core), so NO serial order avoids
* publishing a package before the same-run version it pins. Concurrency
* therefore does not weaken an invariant that serial execution was providing.
*/
/**
* Run `fn` over every item with at most `limit` in flight.
*
* Every item is attempted even if others reject — a half-published release is
* unrecoverable either way (npm refuses to republish a version), so the operator
* is better served by ONE report naming every failure than by a fail-fast that
* hides which packages still need attention. Results are returned in input
* order regardless of completion order.
*/
export async function mapWithConcurrency<T, R>(
items: readonly T[],
limit: number,
fn: (item: T, index: number) => Promise<R>,
): Promise<Array<{ item: T; value?: R; error?: unknown }>> {
if (!Number.isInteger(limit) || limit < 1) {
throw new Error(
`Concurrency limit must be a positive integer, got ${limit}`,
);
}
const results: Array<{ item: T; value?: R; error?: unknown }> = Array.from(
{ length: items.length },
() => ({}) as { item: T; value?: R; error?: unknown },
);
let cursor = 0;
async function worker(): Promise<void> {
// Each worker claims the next index atomically — JS is single-threaded
// between awaits, so the read-then-increment cannot interleave.
while (cursor < items.length) {
const index = cursor++;
const item = items[index];
try {
results[index] = { item, value: await fn(item, index) };
} catch (error) {
results[index] = { item, error };
}
}
}
await Promise.all(
Array.from({ length: Math.min(limit, items.length) }, () => worker()),
);
return results;
}