125 lines
3.7 KiB
TypeScript
125 lines
3.7 KiB
TypeScript
import type { AddressInfo } from "node:net";
|
|
import { once } from "node:events";
|
|
import type { Server } from "node:http";
|
|
type TaskStoreLike = {
|
|
init(): Promise<void>;
|
|
watch(): Promise<void>;
|
|
close(): void;
|
|
};
|
|
|
|
type RuntimeCleanup = () => Promise<void> | void;
|
|
|
|
export interface DesktopLocalRuntime {
|
|
store: TaskStoreLike;
|
|
server: Server;
|
|
port: number;
|
|
cleanup?: RuntimeCleanup;
|
|
}
|
|
|
|
export interface DesktopLocalServerState {
|
|
status: "idle" | "starting" | "ready" | "error";
|
|
port?: number;
|
|
error?: string | null;
|
|
}
|
|
|
|
export class DesktopLocalServerManager {
|
|
private runtime: DesktopLocalRuntime | null = null;
|
|
private state: DesktopLocalServerState = { status: "idle", error: null };
|
|
|
|
constructor(private readonly rootDir: string) {}
|
|
|
|
getState(): DesktopLocalServerState {
|
|
return this.state;
|
|
}
|
|
|
|
getPort(): number | undefined {
|
|
return this.runtime?.port;
|
|
}
|
|
|
|
async start(): Promise<DesktopLocalRuntime> {
|
|
if (this.runtime) {
|
|
this.state = { status: "ready", port: this.runtime.port, error: null };
|
|
return this.runtime;
|
|
}
|
|
|
|
this.state = { status: "starting", error: null };
|
|
|
|
let store: TaskStoreLike | null = null;
|
|
let server: Server | null = null;
|
|
let cleanup: RuntimeCleanup | undefined;
|
|
|
|
try {
|
|
const { TaskStore } = await import("@fusion/core");
|
|
const { CentralCore } = await import("@fusion/core");
|
|
const { createServer } = await import("@fusion/dashboard");
|
|
const { ProjectEngineManager } = await import("@fusion/engine");
|
|
store = new TaskStore(this.rootDir) as TaskStoreLike;
|
|
await store.init();
|
|
await store.watch();
|
|
/*
|
|
* FNXC:DesktopRuntime 2026-06-20-23:39:
|
|
* This legacy desktop local server path still needs to launch project engines so every embedded desktop server follows the same executable-by-default contract.
|
|
*/
|
|
const centralCore = new CentralCore();
|
|
await centralCore.init();
|
|
const engineManager = new ProjectEngineManager(centralCore);
|
|
await engineManager.startAll();
|
|
engineManager.startReconciliation();
|
|
const primaryEngine = [...engineManager.getAllEngines().values()][0];
|
|
const app = createServer(store as never, {
|
|
engine: primaryEngine,
|
|
engineManager,
|
|
centralCore,
|
|
onProjectFirstAccessed: (projectId: string) => engineManager.onProjectAccessed(projectId),
|
|
});
|
|
server = app.listen(0);
|
|
cleanup = async () => {
|
|
await engineManager.stopAll();
|
|
await centralCore.close?.();
|
|
};
|
|
|
|
await Promise.race([
|
|
once(server, "listening"),
|
|
once(server, "error").then(([error]) => {
|
|
throw error;
|
|
}),
|
|
]);
|
|
|
|
const address = server.address() as AddressInfo | null;
|
|
if (!address?.port) {
|
|
throw new Error("Failed to resolve local server port");
|
|
}
|
|
|
|
this.runtime = { store, server, port: address.port, cleanup };
|
|
this.state = { status: "ready", port: address.port, error: null };
|
|
return this.runtime;
|
|
} catch (error) {
|
|
if (server) {
|
|
await new Promise<void>((resolve) => server!.close(() => resolve()));
|
|
}
|
|
await cleanup?.();
|
|
store?.close();
|
|
this.state = {
|
|
status: "error",
|
|
error: error instanceof Error ? error.message : String(error),
|
|
};
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async stop(): Promise<void> {
|
|
if (!this.runtime) {
|
|
this.state = { status: "idle", error: null };
|
|
return;
|
|
}
|
|
|
|
const runtime = this.runtime;
|
|
this.runtime = null;
|
|
|
|
await new Promise<void>((resolve) => runtime.server.close(() => resolve()));
|
|
await runtime.cleanup?.();
|
|
runtime.store.close();
|
|
this.state = { status: "idle", error: null };
|
|
}
|
|
}
|