diff --git a/bun.lock b/bun.lock index e9b09f9..f9f9313 100644 --- a/bun.lock +++ b/bun.lock @@ -1,12 +1,11 @@ { "lockfileVersion": 1, - "configVersion": 1, "workspaces": { "": { "name": "routstrd", "dependencies": { "@cashu/cashu-ts": "^3.1.1", - "@routstr/sdk": "^0.1.2", + "@routstr/sdk": "^0.1.4", "applesauce-core": "^5.1.0", "applesauce-relay": "^5.1.0", "better-sqlite3": "^11.7.2", @@ -31,7 +30,7 @@ "@noble/hashes": ["@noble/hashes@2.0.1", "", {}, "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw=="], - "@routstr/sdk": ["@routstr/sdk@0.1.2", "", { "dependencies": { "@cashu/cashu-ts": "^3.1.1", "applesauce-core": "^5.1.0", "applesauce-relay": "^5.1.0", "rxjs": "^7.8.1", "zustand": "^5.0.5" }, "optionalDependencies": { "better-sqlite3": "^11.7.2" }, "peerDependencies": { "typescript": ">=5.0.0" } }, "sha512-KzcEVAPaQTV9V/AKpAJLQ0YiCjXX4t2Ebz1t+id5dSl/YqTPHZZXm2HA+mT2xKlqcf8GK7hTdj312WxVji1nWQ=="], + "@routstr/sdk": ["@routstr/sdk@0.1.4", "", { "dependencies": { "@cashu/cashu-ts": "^3.1.1", "applesauce-core": "^5.1.0", "applesauce-relay": "^5.1.0", "rxjs": "^7.8.1", "zustand": "^5.0.5" }, "optionalDependencies": { "better-sqlite3": "^11.7.2" }, "peerDependencies": { "typescript": ">=5.0.0" } }, "sha512-4NQV7IcB1UkiO2HQa0as5OBu0v3pZ7TO265oTjMNYPtYMdYOSXjF+uPBcHCRJLfVhnTN4OOCmUQnnhYM0gR6Zw=="], "@scure/base": ["@scure/base@2.0.0", "", {}, "sha512-3E1kpuZginKkek01ovG8krQ0Z44E3DHPjc5S2rjJw9lZn3KSQOs8S7wqikF/AH7iRanHypj85uGyxk0XAyC37w=="], diff --git a/src/daemon.ts b/src/daemon.ts index 792a99a..eef4d74 100644 --- a/src/daemon.ts +++ b/src/daemon.ts @@ -251,13 +251,29 @@ async function main(): Promise { const discoveryAdapter = createDiscoveryAdapterFromStore(store); const providerRegistry = createProviderRegistryFromStore(store); const storageAdapter = createStorageAdapterFromStore(store); - - logger.log("Bootstrapping providers..."); const modelManager = new ModelManager(discoveryAdapter); - const providers = await modelManager.bootstrapProviders(false); - logger.log(`Bootstrapped ${providers.length} providers`); - await modelManager.fetchModels(providers); - logger.log("Provider bootstrap complete."); + + let providerBootstrapPromise: Promise | null = null; + const ensureProvidersBootstrapped = (): Promise => { + if (!providerBootstrapPromise) { + providerBootstrapPromise = (async () => { + logger.log("Bootstrapping providers..."); + const providers = await modelManager.bootstrapProviders(false); + logger.log(`Bootstrapped ${providers.length} providers`); + await modelManager.fetchModels(providers); + logger.log("Provider bootstrap complete."); + })().catch((error) => { + logger.error("Provider bootstrap failed:", error); + throw error; + }); + } + return providerBootstrapPromise; + }; + + // Start bootstrap in background so daemon can become healthy quickly. + void ensureProvidersBootstrapped().catch(() => { + // Error is already logged; keep daemon alive for troubleshooting/retries. + }); let activeMintUrl: string | null = null; let mintUnits: Record = {}; @@ -378,6 +394,7 @@ async function main(): Promise { if (req.method === "GET" && url.pathname === "/models") { try { + await ensureProvidersBootstrapped(); await modelManager.fetchRoutstr21Models(); const models = discoveryAdapter.getRoutstr21Models(); res.writeHead(200, { "Content-Type": "application/json" }); @@ -581,6 +598,7 @@ async function main(): Promise { undefined; try { + await ensureProvidersBootstrapped(); const response = await routeRequests({ modelId, requestBody, @@ -665,6 +683,8 @@ async function main(): Promise { export async function startDaemon(options: { port?: string; provider?: string } = {}): Promise { const args: string[] = []; const port = options.port || "8008"; + const pollIntervalMs = 250; + const startupTimeoutMs = 10 * 60 * 1000; try { const existing = await fetch(`http://localhost:${port}/health`); @@ -697,9 +717,22 @@ export async function startDaemon(options: { port?: string; provider?: string } proc.unref(); + let exitCode: number | null = null; + proc.exited.then((code) => { + exitCode = code; + }); + // Poll until the daemon is healthy - for (let i = 0; i < 100; i++) { - await new Promise((resolve) => setTimeout(resolve, 200)); + const maxPolls = Math.ceil(startupTimeoutMs / pollIntervalMs); + for (let i = 0; i < maxPolls; i++) { + await new Promise((resolve) => setTimeout(resolve, pollIntervalMs)); + + if (exitCode !== null) { + throw new Error( + `Daemon process exited early with code ${exitCode}. Check logs at ${LOG_FILE}` + ); + } + try { const res = await fetch(`http://localhost:${port}/health`); if (res.ok) { @@ -711,7 +744,9 @@ export async function startDaemon(options: { port?: string; provider?: string } } } - throw new Error("Daemon failed to start within 20 seconds"); + throw new Error( + `Daemon failed to start within ${Math.round(startupTimeoutMs / 1000)} seconds. Check logs at ${LOG_FILE}` + ); } // Only auto-run main() when this file is executed directly (not imported) diff --git a/src/utils/logger.ts b/src/utils/logger.ts index 2acf4a6..19f639d 100644 --- a/src/utils/logger.ts +++ b/src/utils/logger.ts @@ -16,7 +16,19 @@ async function writeLog(level: string, ...args: unknown[]) { await ensureLogDir(); const timestamp = new Date().toISOString(); const message = args - .map((a) => (typeof a === "object" ? JSON.stringify(a) : String(a))) + .map((a) => { + if (a instanceof Error) { + return `${a.message}${a.stack ? `\n${a.stack}` : ""}`; + } + if (typeof a === "object") { + try { + return JSON.stringify(a); + } catch { + return String(a); + } + } + return String(a); + }) .join(" "); const line = `[${timestamp}] [${level}] ${message}\n`; try {