Files
ttstd-infinite-canvas/canvas-agent/src/local-runtime-host.ts

139 lines
5.2 KiB
TypeScript

import path from "node:path";
import { createServer, type Server } from "node:http";
import {
CONFIG_DIR,
LOCAL_RUNTIME_DEFAULT_PORT,
VERSION,
ensureRuntimeOwnerId,
loadConfig,
saveConfig,
type LocalRuntimeConfig,
} from "./config.js";
import { createLocalRuntimeApp, type LocalRuntimeModule } from "./local-runtime.js";
import { LocalRuntimeSessionManager } from "./local-runtime-session.js";
import { createCanvasAgentHttpModule } from "./modules/canvas-agent-http.js";
import { createDreaminaHttpModule } from "./modules/dreamina-http.js";
export type StartLocalRuntimeOptions = {
config?: LocalRuntimeConfig;
modules?: readonly LocalRuntimeModule[];
port?: number;
log?: (line: string) => void;
persistConfig?: (config: LocalRuntimeConfig) => void;
};
export function createDefaultLocalRuntimeModules(config: LocalRuntimeConfig): LocalRuntimeModule[] {
return [
createCanvasAgentHttpModule(config),
createDreaminaHttpModule({
ownerId: ensureRuntimeOwnerId(config),
configDir: CONFIG_DIR,
referenceRoots: () => [
path.join(CONFIG_DIR, "codex-workspaces"),
...Object.values(config.canvases ?? {}).map((canvas) => canvas.workspacePath),
],
}),
];
}
export function startLocalRuntime(options: StartLocalRuntimeOptions = {}) {
const config = options.config ?? loadConfig(true);
const persistConfig = options.persistConfig ?? saveConfig;
const requestedPort = options.port ?? (
Number(process.env.PORT)
|| Number(new URL(config.url).port)
|| LOCAL_RUNTIME_DEFAULT_PORT
);
if (!Number.isInteger(requestedPort) || requestedPort < 0 || requestedPort > 65_535) {
throw new Error("Local Runtime port is invalid");
}
const endpoint = requestedPort === 0
? config.url
: `http://127.0.0.1:${requestedPort}`;
const authority = requestedPort === 0 ? "127.0.0.1:0" : `127.0.0.1:${requestedPort}`;
const modules = [...(options.modules ?? createDefaultLocalRuntimeModules(config))];
const sessions = new LocalRuntimeSessionManager({
endpoint,
trustedOrigins: config.trustedWebOrigins,
registrations: config.browserRegistrations,
persistRegistrations: () => persistConfig(config),
onSessionRevoked: (sessionId) => {
for (const module of modules) module.onRuntimeSessionRevoked?.(sessionId);
},
});
const app = createLocalRuntimeApp({
authority,
endpoint,
version: VERSION,
sessionManager: sessions,
modules,
legacyMasterToken: config.token,
legacyOrigins: config.origins ?? [],
});
const server = createServer(app);
const log = options.log ?? console.log;
let modulesDisposed = false;
const disposeModules = async () => {
if (modulesDisposed) return;
modulesDisposed = true;
const errors: unknown[] = [];
for (const module of modules) {
try {
await module.dispose?.();
} catch (error) {
errors.push(error);
}
}
if (errors.length) throw new AggregateError(errors, "Local Runtime module disposal failed");
};
const ready = (async () => {
try {
for (const module of modules) await module.start?.();
server.listen(requestedPort, "127.0.0.1");
await listening(server);
log("Framefield Local Runtime");
log("Runtime is listening on 127.0.0.1");
log("Codex MCP: codex mcp add infinite-canvas -- npx -y @ddcat666/open-ai-canvas-agent mcp");
} catch (startupError) {
sessions.dispose();
const cleanupErrors: unknown[] = [];
try { await closeServer(server); } catch (error) { cleanupErrors.push(error); }
try { await disposeModules(); } catch (error) { cleanupErrors.push(error); }
if (cleanupErrors.length) {
throw new AggregateError([startupError, ...cleanupErrors], "Local Runtime startup failed");
}
throw startupError;
}
})();
let closePromise: Promise<void> | undefined;
const close = () => {
closePromise ??= (async () => {
try { await ready; } catch { /* Startup error remains observable through ready. */ }
sessions.dispose();
const errors: unknown[] = [];
try { await closeServer(server); } catch (error) { errors.push(error); }
try { await disposeModules(); } catch (error) { errors.push(error); }
if (errors.length) throw new AggregateError(errors, "Local Runtime shutdown failed");
})();
return closePromise;
};
return { app, server, sessions, ready, close };
}
function listening(server: Server) {
if (server.listening) return Promise.resolve();
return new Promise<void>((resolve, reject) => {
server.once("listening", resolve);
server.once("error", reject);
});
}
function closeServer(server: Server) {
if (!server.listening) return Promise.resolve();
return new Promise<void>((resolve, reject) => {
server.close((error) => error ? reject(error) : resolve());
});
}