80 lines
3.2 KiB
TypeScript
80 lines
3.2 KiB
TypeScript
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;
|
|
blobs?: Array<{ sha256: string; content: Uint8Array }>;
|
|
recipeJson: Uint8Array;
|
|
payloadJson: Uint8Array;
|
|
}
|
|
|
|
let worker: Worker | null = null;
|
|
let sequence = 0;
|
|
const pending = new Map<number, {
|
|
resolve: (value: unknown) => void;
|
|
reject: (error: Error) => void;
|
|
timer: NodeJS.Timeout;
|
|
}>();
|
|
|
|
function stop(error: Error, current = worker): Promise<number> | 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<T>(kind: 'launch' | 'placement', input: NativeLaunchInput | PlacementOptions): Promise<T> {
|
|
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<T>((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<PreparedNativeLaunch> { return run('launch', input); }
|
|
export function evaluateNativePlacement(input: PlacementOptions): Promise<{ hostId: string; lastHeartbeat: string | null }> {
|
|
return run('placement', input);
|
|
}
|
|
|
|
export async function closeLaunchPreparation(): Promise<void> {
|
|
await stop(new Error('Launch preparation stopped.'));
|
|
}
|