UNPKG

convex

Version:

Client for the Convex Cloud

199 lines (198 loc) 6.75 kB
"use strict"; import boxen from "boxen"; import chalk from "chalk"; import { Command, Option } from "commander"; import httpProxy from "http-proxy"; import path from "path"; import { performance } from "perf_hooks"; import { LOCALHOST_PORT } from "./codegen_templates/clientConfig"; import { getDevDeployment } from "./lib/api"; import { readProjectConfig } from "./lib/config"; import { oneoffContext } from "./lib/context"; import { checkAuthorization, performLogin } from "./lib/login"; import { runPush } from "./lib/push"; import { ensureProjectDirectory, formatDuration } from "./lib/utils"; import { FatalError, WatchContext, Watcher } from "./lib/watch"; export const dev = new Command("dev").description( "Watch the local filesystem. When your Convex functions change, push them to your dev deployment and update generated code." ).option("-v, --verbose", "Show full listing of changes").addOption( new Option( "--typecheck <mode>", `Check TypeScript files with \`tsc --noEmit\`.` ).choices(["enable", "try", "disable"]).default("try") ).addOption(new Option("--trace-events").hideHelp()).addOption(new Option("--once").hideHelp()).addOption(new Option("--admin-key <adminKey>").hideHelp()).addOption(new Option("--url <url>").hideHelp()).addOption( new Option("--codegen <mode>", "Regenerate code in `convex/_generated/`").choices(["enable", "disable"]).default("enable") ).action(async (cmdOptions) => { const ctx = oneoffContext; if (!cmdOptions.url || !cmdOptions.adminKey) { if (!await checkAuthorization(ctx)) { await performLogin(ctx); } } await ensureProjectDirectory(ctx, true); const config = await readProjectConfig(oneoffContext); const projectSlug = config.projectConfig.project; const teamSlug = config.projectConfig.team; let devDeployment; if (!cmdOptions.url || !cmdOptions.adminKey) { devDeployment = await getDevDeployment(oneoffContext, { projectSlug, teamSlug }); } const adminKey = cmdOptions.adminKey ?? devDeployment?.adminKey; const url = cmdOptions.url ?? devDeployment?.url; const options = { adminKey, verbose: !!cmdOptions.verbose, dryRun: false, typecheck: cmdOptions.typecheck, debug: false, codegen: cmdOptions.codegen === "enable", deploymentType: "dev", url }; let watcher; let numFailures = 0; httpProxy.createProxyServer({ target: url, ws: true, changeOrigin: true }).on("error", (err) => { console.log( `Network error connecting to dev deployment: ${err.message}` ); }).listen(LOCALHOST_PORT); const boxedText = chalk.whiteBright.bold("Personal dev deployment ready!") + chalk.white( "\n\nKeep this command running to sync Convex functions when they change." ); const boxenOptions = { align: "center", padding: 1, margin: 1, borderColor: "green", backgroundColor: "#555555" }; if (!cmdOptions.once) { console.log(boxen(boxedText, boxenOptions)); } while (true) { console.log("Preparing Convex functions..."); const start = performance.now(); const ctx2 = new WatchContext(cmdOptions.traceEvents); const config2 = await readProjectConfig(ctx2); if (projectSlug !== config2.projectConfig.project || teamSlug !== config2.projectConfig.team) { console.log("Detected a change in your `convex.json`. Exiting..."); return await ctx2.fatalError(1, "fs"); } try { await runPush(ctx2, options); const end = performance.now(); numFailures = 0; console.log( chalk.green( `Convex functions ready! (${formatDuration(end - start)})` ) ); } catch (e) { if (!(e instanceof FatalError) || !e.reason) { throw e; } if (e.reason === "network") { const delay = nextBackoff(numFailures); numFailures += 1; console.log( chalk.yellow( `Failed due to network error, retrying in ${formatDuration( delay )}...` ) ); await new Promise((resolve) => setTimeout(resolve, delay)); continue; } console.assert(e.reason === "fs"); if (cmdOptions.once) { await ctx2.fatalError(1, "fs"); } } if (cmdOptions.once) { return; } const observations = ctx2.fs.finalize(); if (observations === "invalidated") { console.log("Filesystem changed during push, retrying..."); continue; } if (!watcher) { watcher = new Watcher(observations); await watcher.ready(); } watcher.update(observations); let anyChanges = false; do { await watcher.waitForEvent(); for (const event of watcher.drainEvents()) { if (cmdOptions.traceEvents) { console.log( "Processing", event.name, path.relative("", event.absPath) ); } const result = observations.overlaps(event); if (result.overlaps) { const relPath = path.relative("", event.absPath); if (cmdOptions.traceEvents) { console.log(`${relPath} ${result.reason}, rebuilding...`); } anyChanges = true; break; } } } while (!anyChanges); let deadline = performance.now() + quiescenceDelay; while (true) { const now = performance.now(); if (now >= deadline) { break; } const remaining = deadline - now; if (cmdOptions.traceEvents) { console.log(`Waiting for ${formatDuration(remaining)} to quiesce...`); } const remainingWait = new Promise( (resolve) => setTimeout(() => resolve("timeout"), deadline - now) ); const result = await Promise.race([ remainingWait, watcher.waitForEvent().then(() => "newEvents") ]); if (result === "newEvents") { for (const event of watcher.drainEvents()) { const result2 = observations.overlaps(event); if (result2.overlaps) { if (cmdOptions.traceEvents) { console.log( `Received an overlapping event at ${event.absPath}, delaying push.` ); } deadline = performance.now() + quiescenceDelay; } } } else { console.assert(result === "timeout"); } } } }); const initialBackoff = 500; const maxBackoff = 16e3; const quiescenceDelay = 500; function nextBackoff(prevFailures) { const baseBackoff = initialBackoff * Math.pow(2, prevFailures); const actualBackoff = Math.min(baseBackoff, maxBackoff); const jitter = actualBackoff * (Math.random() - 0.5); return actualBackoff + jitter; } //# sourceMappingURL=dev.js.map