everything-dev
Version:
A consolidated product package for building Module Federation apps with oRPC APIs.
291 lines (289 loc) • 12.7 kB
JavaScript
const require_process_env = require('./env/process-env.cjs');
const require_service_descriptor = require('./service-descriptor.cjs');
const require_dev_log_pipeline = require('./dev-log-pipeline.cjs');
const require_process_registry = require('./process-registry.cjs');
const require_app = require('./app.cjs');
const require_project_env = require('./env/project-env.cjs');
const require_port_ownership = require('./infra/port-ownership.cjs');
const require_orchestrator = require('./orchestrator.cjs');
const require_dev_logs = require('./dev-logs.cjs');
const require_process_kill = require('./process-kill.cjs');
const require_dev_render = require('./components/dev-render.cjs');
let effect = require("effect");
//#region src/dev-session.ts
const adoptOrphanedChildren = (configDir) => {
try {
const entries = require_process_registry.readRegistry().filter((entry) => entry.pid > 1 && entry.configDir === configDir && !require_process_registry.isPidAlive(entry.pid));
if (entries.length === 0) return;
const reaped = [];
for (const entry of entries) for (const childPid of entry.childPids ?? []) {
if (childPid === process.pid) continue;
if (require_process_registry.isPidAlive(childPid)) {
require_process_kill.reapGroup(childPid);
reaped.push(childPid);
}
}
if (reaped.length > 0) console.error(`[Dev] Reaped ${reaped.length} orphaned child process(es) from dead session(s): ${reaped.join(", ")}`);
const deadPids = new Set(entries.map((entry) => entry.pid));
require_process_registry.writeRegistry(require_process_registry.readRegistry().filter((entry) => !deadPids.has(entry.pid)));
} catch {}
};
const isInteractiveSupported = () => {
return process.stdin.isTTY === true && process.stdout.isTTY === true;
};
const STARTUP_ORDER = [
"ui",
"auth",
"api",
"plugin",
"host-build",
"host"
];
const sortByOrder = (packages) => {
return [...packages].sort((a, b) => {
const aIdx = a.startsWith("plugin:") ? STARTUP_ORDER.indexOf("plugin") : STARTUP_ORDER.indexOf(a);
const bIdx = b.startsWith("plugin:") ? STARTUP_ORDER.indexOf("plugin") : STARTUP_ORDER.indexOf(b);
if (aIdx === -1 && bIdx === -1) return 0;
if (aIdx === -1) return 1;
if (bIdx === -1) return -1;
return aIdx - bIdx;
});
};
const runDevSession = (configDir, orchestrator, onShutdownReady) => effect.Effect.gen(function* () {
adoptOrphanedChildren(configDir);
const services = yield* require_service_descriptor.ServiceDescriptorMap;
const runtimeConfig = yield* require_service_descriptor.DevRuntimeConfig;
const orderedPackages = sortByOrder(orchestrator.packages);
const initialProcesses = require_orchestrator.getProcessStates(orderedPackages, services, orchestrator.port);
if (require_dev_log_pipeline.isDebug(require_process_env.getProcessEnv("DEBUG"))) {
yield* effect.Effect.logError(`[DEBUG session] orchestrator.packages: ${orchestrator.packages.join(", ")}`);
yield* effect.Effect.logError(`[DEBUG session] orderedPackages: ${orderedPackages.join(", ")}`);
yield* effect.Effect.logError(`[DEBUG session] services keys: ${[...services.keys()].join(", ")}`);
yield* effect.Effect.logError(`[DEBUG session] initialProcesses: ${initialProcesses.map((p) => `${p.name}=${p.source}`).join(", ")}`);
}
const logger = yield* effect.Effect.promise(() => require_dev_logs.createDevLogger(configDir, orchestrator.description));
const shutdown = yield* effect.Deferred.make();
const effectContext = yield* effect.Effect.context();
const controls = {
requestShutdown: () => {
effect.Effect.runPromiseWith(effectContext)(effect.Deferred.succeed(shutdown, void 0));
},
emergencyKill: () => {},
requestShutdownEscalating: () => {},
forceExit: () => {},
restoreView: () => {},
rearmForceExitTimer: () => {}
};
onShutdownReady?.(controls);
const isWorkspaceChild = require_process_env.getProcessEnv("BOS_WORKSPACE_CHILD") === "1";
let ownedPorts = [];
if (!isWorkspaceChild) {
const regPorts = {};
const addPort = (key, value) => {
if (value != null) regPorts[key] = value;
};
addPort("host", runtimeConfig.host.port);
addPort("api", runtimeConfig.api.port);
addPort("ui", runtimeConfig.ui.port);
addPort("auth", runtimeConfig.auth?.port);
if (runtimeConfig.plugins) for (const [id, plugin] of Object.entries(runtimeConfig.plugins)) {
if (!require_service_descriptor.isAuthMirrorPluginEntry(runtimeConfig.auth, id, plugin)) addPort(`plugin:${id}`, plugin.port);
addPort(`plugin-ui:${id}`, plugin.ui?.port);
}
require_process_registry.registerStandalone({
pid: process.pid,
configDir,
ports: regPorts,
startedAt: yield* effect.Clock.currentTimeMillis,
description: orchestrator.description
});
ownedPorts = Object.values(regPorts);
}
let view = null;
let shouldExportLogs = false;
const logLevel = require_dev_log_pipeline.resolveLogLevel();
const allLogs = [];
const pipeline = require_dev_log_pipeline.createLogPipeline({
level: logLevel,
sinks: {
display: (event) => {
view?.addLog(event.source, event.line, event.level === "error");
},
file: (event) => {
if (orchestrator.noLogs) return;
logger.write(event);
},
export: (event) => {
allLogs.push(event);
}
}
});
const useInteractive = isInteractiveSupported() && orchestrator.interactive !== false;
view = require_dev_render.createDevRenderer(initialProcesses, orchestrator.description, orchestrator.env, () => controls.requestShutdownEscalating(), () => {
shouldExportLogs = true;
controls.requestShutdownEscalating();
}, {
interactive: useInteractive,
onForceExit: () => controls.forceExit()
});
controls.restoreView = () => {
if (useInteractive) view?.unmount();
};
const callbacks = {
onStatus: (name, status, message) => {
view?.updateProcess(name, status, message);
},
onLog: (name, line, isError) => {
pipeline.ingest({
source: name,
line,
isError
});
}
};
const spawned = [];
controls.emergencyKill = () => {
for (const handle of spawned) {
if (handle.pid === void 0) continue;
require_process_kill.reapGroup(handle.pid);
}
};
yield* effect.Effect.addFinalizer(() => effect.Effect.gen(function* () {
controls.rearmForceExitTimer();
yield* effect.Effect.forEach(spawned, (h) => h.kill.pipe(effect.Effect.ignore), { concurrency: "unbounded" });
yield* effect.Effect.sleep("200 millis");
for (const port of ownedPorts) if (!(yield* require_app.probePortBindable(port))) {
const owner = yield* require_port_ownership.ownerOfPort(port);
yield* effect.Effect.logError(`[Dev] Port ${port} still bound after teardown${owner ? ` — pid ${owner.pid} (${owner.command})` : " — owner unknown (lsof unavailable)"}`);
}
if (!isWorkspaceChild) yield* effect.Effect.sync(() => {
try {
require_process_registry.unregisterPid(process.pid);
} catch {}
});
pipeline.flush();
view?.unmount();
if (shouldExportLogs) {
const now = yield* effect.Clock.currentTimeMillis;
const startedMs = allLogs[0]?.timestamp || now;
yield* effect.Effect.log("");
yield* effect.Effect.log("═".repeat(70));
yield* effect.Effect.log(` SESSION LOGS: ${orchestrator.description}`);
yield* effect.Effect.log(` Started: ${effect.DateTime.formatIso(effect.DateTime.makeUnsafe(startedMs))}`);
yield* effect.Effect.log(` Filtered entries: ${allLogs.length} (level: ${logLevel})`);
yield* effect.Effect.log("═".repeat(70));
yield* effect.Effect.log("");
for (const event of allLogs) yield* effect.Effect.log(require_dev_logs.formatLogLine(event));
yield* effect.Effect.log("");
yield* effect.Effect.log("═".repeat(70));
yield* effect.Effect.log(` Full logs saved to: ${logger.logFile}`);
yield* effect.Effect.log("═".repeat(70));
yield* effect.Effect.log("");
}
}));
const startProcess = (pkg) => {
const portOverride = pkg === "host" ? orchestrator.port : void 0;
return require_orchestrator.makeDevProcess(pkg, callbacks, portOverride).pipe(effect.Effect.tap((handle) => effect.Effect.sync(() => spawned.push(handle))), effect.Effect.flatMap((handle) => effect.Effect.acquireRelease(effect.Effect.succeed(handle), () => handle.kill.pipe(effect.Effect.ignore))), effect.Effect.tapError((err) => effect.Effect.sync(() => {
callbacks.onLog(pkg, `Failed to start: ${String(err)}`, true);
callbacks.onStatus(pkg, "error");
})), effect.Effect.orElseSucceed(() => ({
name: pkg,
pid: void 0,
kill: effect.Effect.void,
waitForReady: effect.Effect.void,
waitForExit: effect.Effect.never
})));
};
const startGroup = (packages) => effect.Effect.forEach(packages, startProcess, { concurrency: "unbounded" });
const awaitReady = (pkg, handle) => handle.waitForReady.pipe(effect.Effect.timeout("120 seconds"), effect.Effect.catch((err) => effect.Effect.sync(() => {
callbacks.onLog(pkg, `Timed out or failed: ${err instanceof Error ? err.message : String(err)}`, true);
})));
const nonHostPackages = orderedPackages.filter((pkg) => pkg !== "host");
const hostPackages = orderedPackages.filter((pkg) => pkg === "host");
const startupPhase = effect.Effect.gen(function* () {
const nonHostHandles = yield* startGroup(nonHostPackages);
yield* effect.Effect.forEach(nonHostHandles.map((handle, index) => ({
handle,
pkg: nonHostPackages[index] ?? handle.name
})), ({ handle, pkg }) => awaitReady(pkg, handle), { concurrency: "unbounded" });
const hostHandles = yield* startGroup(hostPackages);
yield* effect.Effect.forEach(hostHandles.map((handle, index) => ({
handle,
pkg: hostPackages[index] ?? handle.name
})), ({ handle, pkg }) => awaitReady(pkg, handle), { concurrency: "unbounded" });
return {
nonHostHandles,
hostHandles
};
});
const startup = yield* effect.Effect.raceFirst(startupPhase, effect.Deferred.await(shutdown).pipe(effect.Effect.as(null)));
if (startup === null) return;
const childPids = [...startup.nonHostHandles, ...startup.hostHandles].map((handle) => Number(handle.pid)).filter((pid) => Number.isFinite(pid) && pid > 1 && pid !== process.pid);
if (!isWorkspaceChild && childPids.length > 0) yield* effect.Effect.sync(() => {
try {
require_process_registry.updateChildPids(process.pid, childPids);
} catch {}
});
yield* effect.Deferred.await(shutdown);
});
const runApp = (configDir, orchestrator, services, runtimeConfig, envGenerated, shellEnv) => {
let controls = null;
let signalCount = 0;
let forceExitTimer = null;
const forceExit = () => {
controls?.restoreView();
console.log("\n[Dev] Force exit");
controls?.emergencyKill();
process.exit(0);
};
const initialPpid = process.ppid;
const orphanWatch = setInterval(() => {
if (process.ppid !== initialPpid) {
console.error("\n[Dev] Parent process died — force exiting (orphaned service tree)");
forceExit();
}
}, 200);
orphanWatch.unref?.();
const requestShutdownEscalating = () => {
signalCount++;
if (signalCount > 1) {
forceExit();
return;
}
controls?.restoreView();
console.log("\n[Dev] Shutting down...");
forceExitTimer = setTimeout(forceExit, 5e3);
forceExitTimer.unref?.();
controls?.requestShutdown();
};
const handleSignal = requestShutdownEscalating;
const program = effect.Effect.scoped(runDevSession(configDir, orchestrator, (sessionControls) => {
controls = sessionControls;
sessionControls.requestShutdownEscalating = requestShutdownEscalating;
sessionControls.rearmForceExitTimer = () => {
if (forceExitTimer) clearTimeout(forceExitTimer);
forceExitTimer = setTimeout(forceExit, 5e3);
forceExitTimer.unref?.();
};
sessionControls.forceExit = forceExit;
})).pipe(effect.Effect.provide(effect.Layer.mergeAll(require_service_descriptor.ServiceDescriptorMapLive(services), require_service_descriptor.DevRuntimeConfigLive(runtimeConfig), require_service_descriptor.DevGeneratedEnvLive(envGenerated), require_project_env.ShellEnvLive(shellEnv))), effect.Effect.catchDefect((defect) => effect.Effect.logError("[Dev] Unhandled defect in orchestrator:", defect).pipe(effect.Effect.andThen(effect.Effect.die(defect)))));
process.on("SIGINT", handleSignal);
process.on("SIGTERM", handleSignal);
effect.Effect.runPromiseExit(program).then((exit) => {
clearInterval(orphanWatch);
if (forceExitTimer) clearTimeout(forceExitTimer);
process.exit(effect.Exit.isSuccess(exit) ? 0 : 1);
}).catch((error) => {
clearInterval(orphanWatch);
if (forceExitTimer) clearTimeout(forceExitTimer);
console.error("[Dev] Failed to observe application exit:", error);
process.exit(1);
});
};
const devApp = runApp;
const startApp = runApp;
//#endregion
exports.devApp = devApp;
exports.runDevSession = runDevSession;
exports.startApp = startApp;
//# sourceMappingURL=dev-session.cjs.map