convex
Version:
Client for the Convex Cloud
199 lines (198 loc) • 6.75 kB
JavaScript
;
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