Skip to content

perf(cli): parallelize github/npm/llms-txt fetches in sync command#8

Merged
amondnet merged 6 commits into
mainfrom
2-perfcli-parallelize-github+npm-fetches-in-sync-command
Apr 7, 2026
Merged

perf(cli): parallelize github/npm/llms-txt fetches in sync command#8
amondnet merged 6 commits into
mainfrom
2-perfcli-parallelize-github+npm-fetches-in-sync-command

Conversation

@amondnet

@amondnet amondnet commented Apr 7, 2026

Copy link
Copy Markdown
Contributor

Closes #2

Summary

syncCmd previously iterated config.docs strictly serially, so wall time grew linearly with the number of github/npm entries. This change parallelizes network-bound work with bounded concurrency while preserving every existing safety invariant.

  • runWithConcurrency (src/concurrency.ts) — tiny inline limiter, no new dependency. Preserves input order, caps in-flight workers, propagates errors to the caller.
  • syncEntry extracted from the syncCmd loop. Same write order (saveDocs → addDocEntry → upsertLockEntry → removeDocs(old) → generateSkill), same log lines, never throws (failures returned as 'failed' status so a single bad entry can't abort the batch).
  • Partition config.docs into a parallel-safe group (github, npm, llms-txt, concurrency=5) and a serial group (web — kept serial to remain polite to upstream documentation servers).
  • runSync(projectDir, options) exported so the partition + concurrency behavior is testable without a child process. The bare syncCmd definition is now a thin wrapper.
  • runMain guarded behind an entry-point check so the module is importable from tests without triggering CLI execution.

Concurrency safety audit (T-2)

addDocEntry and upsertLockEntry both perform fully synchronous read-modify-write on the .ask/ files (fs.readFileSync → mutate → fs.writeFileSync). There is no await between read and write, so Node's single-threaded event loop guarantees they execute atomically with respect to each other even when called from parallel workers. Documented inline next to syncEntry.

Tests

  • test/concurrency.test.ts — 8 cases (empty / single / order preservation / limit=1 strict serial / bounded parallel / unbounded / capped at items.length / error propagation)
  • test/sync-partition.test.ts — 5 cases (mixed parallel+serial with overlap assertion, cap at 5 with 12 entries, catch-and-continue with one failing entry, empty config, all-web serial)

All 13 new tests pass. Lint and build clean.

Out of scope (per spec)

  • Generic readJson<T>/writeJson<T> helpers in io.ts
  • Batching lock+config writes into a single read/write at end of sync
  • --concurrency CLI flag (hard-coded 5 for now)

Test plan

  • `bun run --cwd packages/cli lint`
  • `bun run --cwd packages/cli build`
  • `bun test test/concurrency.test.ts` (8/8)
  • `bun test test/sync-partition.test.ts` (5/5)
  • Manual smoke: real `.ask/config.json` with multiple github entries → confirm wall-clock improvement vs main

amondnet added 3 commits April 7, 2026 22:21
Refs #2

`syncCmd` previously iterated `config.docs` strictly serially. With many
github entries each requiring a tarball download + ls-remote round-trip
(~3-5s each), total wall time grew linearly with entry count.

This change:

* Adds `runWithConcurrency` (src/concurrency.ts) — a tiny inline limiter
  that preserves input order, caps in-flight workers, and propagates
  errors to the caller (catch-and-continue is layered above).
* Extracts `syncEntry` from the syncCmd loop. Same write order as
  before (saveDocs → addDocEntry → upsertLockEntry → removeDocs(old)
  → generateSkill); same log lines; never throws (failures returned
  as 'failed' status).
* Partitions `config.docs` into a parallel-safe group (github / npm /
  llms-txt, concurrency=5) and a serial group (web — kept serial to
  remain polite to upstream documentation servers).
* Exports `runSync(projectDir, options)` so the partition + concurrency
  behavior is testable without spawning a child process. The bare
  `syncCmd` definition is now a thin wrapper.
* Guards `runMain(main)` behind an entry-point check so the module is
  importable from tests without triggering CLI execution.

Concurrency safety audit (T-2): `addDocEntry` and `upsertLockEntry`
both perform fully synchronous read-modify-write on the .ask files.
There is no `await` between read and write, so Node's single-threaded
event loop guarantees they execute atomically with respect to each
other even when called from parallel workers.

Tests:
* test/concurrency.test.ts — 8 cases (empty / single / order
  preservation / limit=1 serial / bounded parallel / unbounded /
  capped at items.length / error propagation)
* test/sync-partition.test.ts — 5 cases (mixed parallel+serial,
  cap at 5, catch-and-continue with one failure, empty config,
  all-web serial)

Out of scope (per spec): generic readJson/writeJson helpers in io.ts,
batching lock+config writes, --concurrency CLI flag.
@amondnet amondnet linked an issue Apr 7, 2026 that may be closed by this pull request
@vercel

vercel Bot commented Apr 7, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
ask Error Error Apr 7, 2026 1:41pm

Request Review

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Apr 7, 2026

Copy link
Copy Markdown

Deploying ask-registry with  Cloudflare Pages  Cloudflare Pages

Latest commit: 823caae
Status:⚡️  Build in progress...

View logs

The previous `file://${process.argv[1]}` template-string comparison
broke on Windows (backslash paths produce `file://C:\path\…` which
never matches `file:///C:/path/…`) and on POSIX paths containing
spaces or unicode (no URL encoding). Use `pathToFileURL` instead.

Caught by /review:code-review (confidence 85).
@amondnet amondnet merged commit c4beeb0 into main Apr 7, 2026
2 of 4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

perf(cli): parallelize github+npm fetches in sync command

1 participant