import { Worker } from 'node:worker_threads'; import { resolve } from 'node:path'; import { config } from '../config.js'; import { db } from '../db/index.js'; import type { NativeInstance } from '../backends/native.js'; import type { LaunchRecipePlan } from '../fleet/launch-recipes.js'; import type { PlacementOptions } from '../hosts/capacity.js'; export interface NativeLaunchInput { game: 't5' | 't6'; mapSlug: string; native?: LaunchRecipePlan['native']; instance: NativeInstance; serverKey: string; rconPassword: string; rebuild: boolean; safetyOverride?: boolean; } export interface PreparedNativeLaunch { recipe: LaunchRecipePlan; recipeSummary: string; recipeJson: Uint8Array; payloadJson: Uint8Array; } let worker: Worker | null = null; let sequence = 0; const pending = new Map void; reject: (error: Error) => void; timer: NodeJS.Timeout; }>(); function stop(error: Error, current = worker): Promise | undefined { if (current !== worker) return; worker = null; for (const request of pending.values()) { clearTimeout(request.timer); request.reject(error); } pending.clear(); return current?.terminate(); } /** The reader and JSON encoder run off the controller's request thread. */ function run(kind: 'launch' | 'placement', input: NativeLaunchInput | PlacementOptions): Promise { db(); // Migrations must finish before the independent read-only connection opens. if (!worker) { const current = worker = new Worker(resolve(process.env.NODE_ENV === 'production' ? 'build/launch/worker.mjs' : '.svelte-kit/launch/worker.mjs'), { workerData: { dbPath: resolve(config.dbPath) } }); current.on('message', ({ id, value, error }: { id: number; value: unknown; error?: string }) => { if (current !== worker) return; const request = pending.get(id); if (!request) return; pending.delete(id); clearTimeout(request.timer); if (error) request.reject(new Error(error)); else request.resolve(value); if (!pending.size) current.unref(); }); current.on('error', error => { void stop(error, current); }); current.on('exit', () => { void stop(new Error('Launch preparation worker stopped; preparation will retry.'), current); }); } worker.ref(); const id = ++sequence; return new Promise((resolve, reject) => { const timer = setTimeout(() => { void stop(new Error('Launch preparation timed out; preparation will retry.')); }, 60_000); pending.set(id, { resolve: value => resolve(value as T), reject, timer }); worker!.postMessage({ id, kind, input }); }); } export function prepareNativeLaunch(input: NativeLaunchInput): Promise { return run('launch', input); } export function evaluateNativePlacement(input: PlacementOptions): Promise<{ hostId: string; lastHeartbeat: string | null }> { return run('placement', input); } export async function closeLaunchPreparation(): Promise { await stop(new Error('Launch preparation stopped.')); }