Keep native collection launches responsive
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
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';
|
||||
|
||||
export interface NativeLaunchInput {
|
||||
game: 't5' | 't6';
|
||||
mapSlug: string;
|
||||
native?: LaunchRecipePlan['native'];
|
||||
instance: NativeInstance;
|
||||
serverKey: string;
|
||||
rconPassword: string;
|
||||
rebuild: boolean;
|
||||
}
|
||||
|
||||
export interface PreparedNativeLaunch {
|
||||
recipe: LaunchRecipePlan;
|
||||
recipeSummary: string;
|
||||
recipeJson: Uint8Array;
|
||||
payloadJson: Uint8Array;
|
||||
}
|
||||
|
||||
let worker: Worker | null = null;
|
||||
let sequence = 0;
|
||||
const pending = new Map<number, {
|
||||
resolve: (value: PreparedNativeLaunch) => 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. */
|
||||
export function prepareNativeLaunch(input: NativeLaunchInput): Promise<PreparedNativeLaunch> {
|
||||
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: PreparedNativeLaunch; 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, reject, timer });
|
||||
worker!.postMessage({ id, input });
|
||||
});
|
||||
}
|
||||
|
||||
export async function closeLaunchPreparation(): Promise<void> {
|
||||
await stop(new Error('Launch preparation stopped.'));
|
||||
}
|
||||
Reference in New Issue
Block a user