perf: optimize app startup and background lifecycles
This commit is contained in:
1 parent
31332f6d76
commit
927e13349b
121 files changed
+4359
-1400
No files matched your search
@@ -0,0 +1,191 @@
|
||||
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<UtilityMessage>;
|
||||
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<typeof ProjectReleaseBuildError>[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<UtilityProcess>();
|
||||
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<ReleaseJobPreparation>((resolve, reject) => {
|
||||
let settled = false;
|
||||
let cancelTimer: ReturnType<typeof setTimeout> | 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();
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user