Skip to content

Commit a820577

Browse files
perf(setup): download an org in about 2s instead of 30s (#93)
## Value **V.A.L.U.E. tier:** micro. It's a mechanical change of 36 lines across 3 files, and the before/after timings below prove it. - **Problem:** connecting an org through the setup wizard is slow. Picking "All" took **31.6s** on an org of about 300 resources. Each API request returns in about 90ms and local work is under a second, so nearly all of that was fixed waiting. - **Who it affects:** everyone running `npm run setup`, including every [`npx create-vapi-app`](https://www.npmjs.com/package/create-vapi-app) user, plus anyone running `npm run pull`. ## How it works 1. **No `--force` pause for setup's own pulls.** `pull --force` warns and waits 2s so someone typing it by hand can Ctrl+C. The wizard ran one `--force` pull per resource type, so 9 types meant 18s of pauses. setup now sets `VAPI_GITOPS_FORCE_PAUSE=off` for the pulls it starts, following the `VAPI_GITOPS_ORG_PIN=off` convention. A hand-typed `--force` still pauses. 2. **No request gap on GETs.** `api.ts` waited 700ms between every request, reads included, which was about 9 of a plain pull's 11 seconds. Reads now skip the gap. A 429 is still retried with backoff (5 tries from 2s), as the config-free `vapi-client.ts` already does. Writes (POST, PATCH, DELETE) keep the gap. 3. **One pull for "All".** Picking "All" ran 10 processes, each re-fetching credentials and its type's full list. It now runs the same two steps as non-interactive setup: a bindings-only pull, then one full `--force --skip-bindings` pull. "Let me pick…" still pulls by `--id` per type. 4. **Org folder created before the first pull.** The org pin (`.vapi-org.json`) is only written into a folder that already exists. Non-interactive setup creates `resources/<org>/` first, but the wizard didn't, so the pin used to come from its second per-type pull. With a single pull, nothing would have written it, so the wizard now creates the folder first too. ## Verification These are the exact command sequences each path runs, timed against a real org (`scott-dev`: 8 tools, 13 assistants, 8 structured outputs, 2 squads, 11 personalities, 118 scenarios, 118 simulations, 11 suites, 8 evals). They used read-only pulls in a throwaway copy of the repo. | Path | Before | After | |---|---|---| | Wizard, "All" | 31.6s | **2.2–2.5s** | | Wizard, "Let me pick…" with everything picked | 31.6s | 6.2s | | Non-interactive setup / `npm run pull` | 11.1s | ~2.4s | | Hand-typed `npm run pull -- <org> --force` | 11.1s | 3.8s (still pauses 2s) | - **Output unchanged:** "All" via one pull and "pick everything" via per-type pulls wrote identical trees, 298 files, `.vapi-org.json` included. - **No rate limiting:** none of the runs logged a 429 retry. Separately, 11 concurrent list requests against production all returned 200. - `npm run build` passes, and `npm test` passes 584/584. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
2 parents 34cb5a3 + 7979ad2 commit a820577

3 files changed

Lines changed: 36 additions & 6 deletions

File tree

‎src/api.ts‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,10 @@ function formatBodyPreview(body: Record<string, unknown>): string {
103103
// existing importers and `instanceof` checks see one class.
104104
export { VapiApiError };
105105

106-
const REQUEST_DELAY_MS = 700; // Delay between requests to avoid rate limits
106+
// Minimum gap between writes, to stay under rate limits. Reads skip it: a pull
107+
// is a handful of list requests, and a 700ms gap before each one was most of a
108+
// pull's run time. A 429 on a read is still retried with backoff.
109+
const REQUEST_DELAY_MS = 700;
107110

108111
let lastRequestTime = 0;
109112

@@ -190,7 +193,6 @@ export async function vapiGet<T = unknown>(endpoint: string): Promise<T> {
190193
const url = `${VAPI_BASE_URL}${endpoint}`;
191194

192195
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
193-
await throttle();
194196
const response = await fetch(url, {
195197
method: "GET",
196198
headers: {

‎src/pull.ts‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1291,7 +1291,13 @@ export async function runPull(options: PullOptions = {}): Promise<PullResult> {
12911291
const bothDivergedResources: BothDivergedResource[] = [];
12921292
let idsByType: Map<ResourceType, string[]> | undefined;
12931293

1294-
if (force && process.env.CI !== "true") {
1294+
// setup turns the pause off for the pulls it runs itself: the warning is for
1295+
// someone typing --force, and the wizard can run one --force pull per type.
1296+
if (
1297+
force &&
1298+
process.env.CI !== "true" &&
1299+
process.env.VAPI_GITOPS_FORCE_PAUSE !== "off"
1300+
) {
12951301
console.warn(
12961302
"\n⚠️ --force will overwrite local files without showing you direction labels or surfacing 3-way conflicts.",
12971303
);

‎src/setup.ts‎

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -304,15 +304,17 @@ async function deleteExistingOrg(slug: string): Promise<void> {
304304
// Pull integration
305305
// ─────────────────────────────────────────────────────────────────────────────
306306

307+
// "all" downloads everything with one full pull rather than one pull per type,
308+
// each of which starts a process and re-fetches credentials and its list.
307309
function invokePull(
308310
slug: string,
309-
selectedIds: Set<string>,
311+
selectedIds: Set<string> | "all",
310312
token: string,
311313
baseUrl: string,
312314
): void {
313315
// Group selected resources by type so we can pass --id per invocation
314316
const byType = new Map<string, string[]>();
315-
for (const id of selectedIds) {
317+
for (const id of selectedIds === "all" ? [] : selectedIds) {
316318
const sep = id.indexOf("::");
317319
const typeKey = id.substring(0, sep);
318320
const uuid = id.substring(sep + 2);
@@ -332,6 +334,7 @@ function invokePull(
332334
VAPI_PRIVATE_API_KEY: token,
333335
VAPI_TOKEN: token,
334336
VAPI_BASE_URL: baseUrl,
337+
VAPI_GITOPS_FORCE_PAUSE: "off",
335338
};
336339

337340
execSync(`tsx src/pull.ts ${slug} --bootstrap --bindings-only`, {
@@ -340,6 +343,15 @@ function invokePull(
340343
env,
341344
});
342345

346+
if (selectedIds === "all") {
347+
execSync(`tsx src/pull.ts ${slug} --force --skip-bindings`, {
348+
cwd: BASE_DIR,
349+
stdio: "inherit",
350+
env,
351+
});
352+
return;
353+
}
354+
343355
for (const [typeKey, uuids] of byType) {
344356
const idArgs = uuids.flatMap((id) => ["--id", id]);
345357
const cmd = [
@@ -704,6 +716,7 @@ async function main(): Promise<void> {
704716

705717
// selectedIds: "typeKey::resourceUUID"
706718
let selectedIds: Set<string>;
719+
let downloadAll = false;
707720

708721
// eslint-disable-next-line no-constant-condition
709722
while (true) {
@@ -738,6 +751,7 @@ async function main(): Promise<void> {
738751

739752
selectedIds = new Set(picked);
740753
} else {
754+
downloadAll = true;
741755
selectedIds = new Set(
742756
nonEmpty.flatMap((snap) =>
743757
snap.resources.map((r) => `${snap.key}::${r.id as string}`),
@@ -796,10 +810,18 @@ async function main(): Promise<void> {
796810

797811
await writeEnvFile(slug, trimmedKey, vapiBaseUrl);
798812
console.log(c.green(` ✓ Created .env.${slug}\n`));
813+
// Before the first pull, as non-interactive setup does: the org pin
814+
// (.vapi-org.json) is only written into a folder that already exists.
815+
await mkdir(resourceDir, { recursive: true });
799816

800817
console.log(c.bold(" Downloading...\n"));
801818

802-
invokePull(slug, selectedIds, trimmedKey, vapiBaseUrl);
819+
invokePull(
820+
slug,
821+
downloadAll ? "all" : selectedIds,
822+
trimmedKey,
823+
vapiBaseUrl,
824+
);
803825

804826
// ── Done ──────────────────────────────────────────────────────────────
805827

0 commit comments

Comments
 (0)