import { randomUUID } from 'node:crypto'; import { existsSync } from 'node:fs'; import { join } from 'node:path'; import { utilityProcess, type UtilityProcess } from 'electron'; import { prepareProjectReleaseSummary, ProjectReleaseBuildError, type ReleaseBuildProgress, type ReleaseBuildRuntimeContext, } from './project-release-builder'; import type { ReleaseJobPrepare, ReleaseJobPreparation } from './release-job'; type UtilityStartMessage = { type: 'start'; jobId: string; projectPath: string; clientVersion: string; runtimeContext: ReleaseBuildRuntimeContext; }; type UtilityCancelMessage = { type: 'cancel'; jobId: string; }; type UtilityMessage = | { type: 'progress'; jobId: string; progress: ReleaseBuildProgress } | { type: 'complete'; jobId: string; packageSummary: ReleaseJobPreparation['sourceArchive']['summary']; contract: ReleaseJobPreparation['contract']; } | { type: 'error'; jobId: string; code: string; message: string; }; type UtilityProcessFactoryOptions = { workerPath?: string; runtimeContext: ReleaseBuildRuntimeContext; /** Useful for diagnostics and a safe development fallback before a build. */ forceDirect?: boolean; }; export type ReleaseUtilityPreparer = { prepare: ReleaseJobPrepare; dispose(): void; }; const knownBuildCodes = new Set([ 'LOCAL_BUILD_RUNTIME_UNAVAILABLE', 'LOCAL_BUILD_FAILED', 'LOCAL_BUILD_TIMEOUT', 'LOCAL_BUILD_CANCELLED', 'LOCAL_BUILD_OUTPUT_MISSING', 'LOCAL_BUILD_BUSY', ]); function workerMessage(value: unknown): UtilityMessage | null { const candidate = value && typeof value === 'object' && 'data' in value ? (value as { data?: unknown }).data : value; if (!candidate || typeof candidate !== 'object') return null; const message = candidate as Partial; return typeof message.type === 'string' && typeof message.jobId === 'string' ? message as UtilityMessage : null; } function toBuildError(code: string, message?: string): ProjectReleaseBuildError { const safeCode = knownBuildCodes.has(code) ? code as ConstructorParameters[0] : 'LOCAL_BUILD_FAILED'; const error = new ProjectReleaseBuildError(safeCode); if (message && message !== error.message) { error.message = message; } return error; } function defaultWorkerPath(): string { // Main is bundled into dist-electron/main; the utility entry is emitted next // to it so the packaged asar contains an explicit, inspectable worker. return join(__dirname, '../utility/release-utility-worker.js'); } export function createReleaseUtilityPreparer( options: UtilityProcessFactoryOptions, ): ReleaseUtilityPreparer { const workers = new Set(); const workerPath = options.workerPath ?? defaultWorkerPath(); const canFork = !options.forceDirect && process.env.NIANCODE_DISABLE_RELEASE_UTILITY !== '1' && existsSync(workerPath); const prepare: ReleaseJobPrepare = async (input) => { if (!canFork) { return await prepareProjectReleaseSummary({ ...input, runtimeContext: { ...options.runtimeContext, ...input.runtimeContext }, }); } const child = utilityProcess.fork(workerPath, [], { serviceName: 'Makelore Release Build', stdio: 'ignore', }); workers.add(child); const jobId = randomUUID(); return await new Promise((resolve, reject) => { let settled = false; let cancelTimer: ReturnType | null = null; const cleanup = (): void => { workers.delete(child); if (cancelTimer !== null) clearTimeout(cancelTimer); input.signal?.removeEventListener('abort', abort); }; const finish = (callback: () => void): void => { if (settled) return; settled = true; cleanup(); callback(); }; const abort = (): void => { if (settled) return; child.postMessage({ type: 'cancel', jobId } satisfies UtilityCancelMessage); cancelTimer = setTimeout(() => { if (!settled) { child.kill(); finish(() => reject(new ProjectReleaseBuildError('LOCAL_BUILD_CANCELLED'))); } }, 2_000); }; child.on('message', (value) => { const message = workerMessage(value); if (!message || message.jobId !== jobId) return; if (message.type === 'progress') { input.onProgress?.(message.progress); return; } if (message.type === 'complete') { finish(() => resolve({ sourceArchive: { summary: message.packageSummary }, contract: message.contract, dispose: async () => undefined, })); return; } if (message.type === 'error') { finish(() => reject(toBuildError(message.code, message.message))); } }); child.on('error', () => { finish(() => reject(new ProjectReleaseBuildError('LOCAL_BUILD_RUNTIME_UNAVAILABLE'))); }); child.on('exit', (code) => { if (settled) return; finish(() => reject( input.signal?.aborted ? new ProjectReleaseBuildError('LOCAL_BUILD_CANCELLED') : new ProjectReleaseBuildError(code === 0 ? 'LOCAL_BUILD_FAILED' : 'LOCAL_BUILD_RUNTIME_UNAVAILABLE'), )); }); input.signal?.addEventListener('abort', abort, { once: true }); child.postMessage({ type: 'start', jobId, projectPath: input.projectPath, clientVersion: input.clientVersion, runtimeContext: { ...options.runtimeContext, ...input.runtimeContext }, } satisfies UtilityStartMessage); }); }; return { prepare, dispose: () => { for (const child of workers) child.kill(); workers.clear(); }, }; }