core/apps/webapp/app/utils/startup.ts
2025-08-01 10:33:59 +05:30

239 lines
6.7 KiB
TypeScript

import { logger } from "~/services/logger.service";
import { fetchAndSaveStdioIntegrations } from "~/trigger/utils/mcp";
import { initNeo4jSchemaOnce } from "~/lib/neo4j.server";
import { env } from "~/env.server";
// Global flag to ensure startup only runs once per server process
let startupInitialized = false;
/**
* Initialize all startup services once per server process
* Safe to call multiple times - will only run initialization once
*/
export async function initializeStartupServices() {
if (startupInitialized) {
return;
}
// Wait for TRIGGER_API_URL/login to be available, up to 1 minute
async function waitForTriggerLogin(
url: string,
timeoutMs = 60000,
intervalMs = 2000,
) {
const start = Date.now();
while (Date.now() - start < timeoutMs) {
try {
const res = await fetch(`${url}/login`, { method: "GET" });
if (res.ok) {
return;
}
} catch (e) {
// ignore, will retry
}
await new Promise((resolve) => setTimeout(resolve, intervalMs));
}
// If we get here, the service is still not available
console.error(
`TRIGGER_API_URL/login is not available after ${timeoutMs / 1000} seconds. Exiting process.`,
);
process.exit(1);
}
try {
const triggerApiUrl = env.TRIGGER_API_URL;
if (triggerApiUrl) {
await waitForTriggerLogin(triggerApiUrl);
await addEnvVariablesInTrigger();
} else {
console.error("TRIGGER_API_URL is not set in environment variables.");
process.exit(1);
}
} catch (e) {
console.error(e);
console.error("Trigger is not configured");
process.exit(1);
}
try {
logger.info("Starting application initialization...");
// Initialize Neo4j schema
await initNeo4jSchemaOnce();
logger.info("Neo4j schema initialization completed");
await fetchAndSaveStdioIntegrations();
logger.info("Stdio integrations initialization completed");
startupInitialized = true;
logger.info("Application initialization completed successfully");
} catch (error) {
logger.error("Failed to initialize startup services:", { error });
// Don't mark as initialized if there was an error, allow retry
}
}
export function getDatabaseUrl(dbName: string): string {
const { DATABASE_URL } = env;
if (!dbName) {
throw new Error("dbName is required");
}
// Parse the DATABASE_URL and replace the database name
try {
const url = new URL(DATABASE_URL);
// The pathname starts with a slash, e.g. "/echo"
url.pathname = `/${dbName}`;
return url.toString();
} catch (err) {
throw new Error(`Invalid DATABASE_URL format: ${err}`);
}
}
const Keys = [
"API_BASE_URL",
"DATABASE_URL",
"EMBEDDING_MODEL",
"MODEL",
"ENCRYPTION_KEY",
"NEO4J_PASSWORD",
"NEO4J_URI",
"NEO4J_USERNAME",
"OPENAI_API_KEY",
];
export async function addEnvVariablesInTrigger() {
const {
APP_ORIGIN,
TRIGGER_DB,
EMBEDDING_MODEL,
MODEL,
ENCRYPTION_KEY,
NEO4J_PASSWORD,
NEO4J_URI,
NEO4J_USERNAME,
OPENAI_API_KEY,
TRIGGER_PROJECT_ID,
TRIGGER_API_URL,
TRIGGER_SECRET_KEY,
} = env;
const DATABASE_URL = getDatabaseUrl(TRIGGER_DB);
// Helper to replace 'localhost' with 'host.docker.internal'
function replaceLocalhost(val: string | undefined): string | undefined {
if (typeof val !== "string") return val;
return val.replace(/localhost/g, "host.docker.internal");
}
// Map of key to value from env, replacing 'localhost' as needed
const envVars: Record<string, string> = {
API_BASE_URL: replaceLocalhost(APP_ORIGIN) ?? "",
DATABASE_URL: replaceLocalhost(DATABASE_URL) ?? "",
EMBEDDING_MODEL: replaceLocalhost(EMBEDDING_MODEL) ?? "",
MODEL: replaceLocalhost(MODEL) ?? "",
ENCRYPTION_KEY: replaceLocalhost(ENCRYPTION_KEY) ?? "",
NEO4J_PASSWORD: replaceLocalhost(NEO4J_PASSWORD) ?? "",
NEO4J_URI: replaceLocalhost(NEO4J_URI) ?? "",
NEO4J_USERNAME: replaceLocalhost(NEO4J_USERNAME) ?? "",
OPENAI_API_KEY: replaceLocalhost(OPENAI_API_KEY) ?? "",
};
const envName = "prod";
const apiBase = `${TRIGGER_API_URL}/api/v1`;
const envVarsUrl = `${apiBase}/projects/${TRIGGER_PROJECT_ID}/envvars/${envName}`;
try {
logger.info("Fetching current environment variables from Trigger...", {
envVarsUrl,
});
// Fetch current env vars
const response = await fetch(envVarsUrl, {
method: "GET",
headers: {
Authorization: `Bearer ${TRIGGER_SECRET_KEY}`,
"Content-Type": "application/json",
Accept: "application/json",
},
});
if (!response.ok) {
logger.error("Failed to fetch env vars from Trigger", {
status: response.status,
statusText: response.statusText,
});
throw new Error(
`Failed to fetch env vars: ${response.status} ${response.statusText}`,
);
}
const currentVars: Array<{ name: string; value: string }> =
await response.json();
logger.info("Fetched current env vars from Trigger", {
count: currentVars.length,
});
// Build a set of existing env var names
const existingNames = new Set(currentVars.map((v) => v.name));
// Find missing keys
const missingKeys = Keys.filter((key) => !existingNames.has(key));
if (missingKeys.length === 0) {
logger.info("No missing environment variables to add in Trigger.");
} else {
logger.info("Missing environment variables to add in Trigger", {
missingKeys,
});
}
// For each missing key, POST to create
for (const key of missingKeys) {
const value = envVars[key];
if (typeof value === "undefined") {
logger.warn(
`Environment variable ${key} is undefined in envVars, skipping.`,
);
continue;
}
logger.info(`Creating environment variable in Trigger: ${key}`);
const createRes = await fetch(envVarsUrl, {
method: "POST",
headers: {
Authorization: `Bearer ${TRIGGER_SECRET_KEY}`,
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify({
name: key,
value,
}),
});
if (!createRes.ok) {
logger.error("Failed to create env var in Trigger", {
key,
status: createRes.status,
statusText: createRes.statusText,
});
throw new Error(
`Failed to create env var ${key}: ${createRes.status} ${createRes.statusText}`,
);
} else {
logger.info(
`Successfully created environment variable in Trigger: ${key}`,
);
}
}
logger.info("addEnvVariablesInTrigger completed successfully.");
} catch (err) {
logger.error("Error in addEnvVariablesInTrigger", { error: err });
throw err;
}
}