perf: optimize app startup and background lifecycles
This commit is contained in:
@@ -8,7 +8,7 @@ const require = createRequire(import.meta.url);
|
||||
const OUTPUT_LIMIT = 64 * 1024;
|
||||
|
||||
export class PublishRuntimeError extends Error {
|
||||
constructor(readonly code: 'LOCAL_BUILD_RUNTIME_UNAVAILABLE' | 'LOCAL_BUILD_FAILED' | 'LOCAL_BUILD_TIMEOUT') {
|
||||
constructor(readonly code: 'LOCAL_BUILD_RUNTIME_UNAVAILABLE' | 'LOCAL_BUILD_FAILED' | 'LOCAL_BUILD_TIMEOUT' | 'LOCAL_BUILD_CANCELLED') {
|
||||
super(code);
|
||||
this.name = 'PublishRuntimeError';
|
||||
}
|
||||
@@ -20,6 +20,13 @@ export type PublishRuntime = {
|
||||
npmVersion: string;
|
||||
};
|
||||
|
||||
export type PublishRuntimeContext = {
|
||||
isPackaged: boolean;
|
||||
resourcesPath: string;
|
||||
};
|
||||
|
||||
const cachedPublishRuntimes = new Map<string, Promise<PublishRuntime>>();
|
||||
|
||||
function childEnvironment(): NodeJS.ProcessEnv {
|
||||
const allowed = ['ALLUSERSPROFILE', 'APPDATA', 'COMMONPROGRAMFILES', 'COMMONPROGRAMFILES(X86)',
|
||||
'COMMONPROGRAMW6432', 'COMSPEC', 'HOME', 'HOMEDRIVE', 'HOMEPATH', 'LOCALAPPDATA',
|
||||
@@ -54,8 +61,13 @@ export async function runElectronNode(input: {
|
||||
args: string[];
|
||||
cwd?: string;
|
||||
deadlineMs: number;
|
||||
signal?: AbortSignal;
|
||||
}): Promise<{ stdout: string; stderr: string }> {
|
||||
return await new Promise((resolve, reject) => {
|
||||
if (input.signal?.aborted) {
|
||||
reject(new PublishRuntimeError('LOCAL_BUILD_CANCELLED'));
|
||||
return;
|
||||
}
|
||||
const child = spawn(process.execPath, input.args, {
|
||||
cwd: input.cwd,
|
||||
env: childEnvironment(),
|
||||
@@ -70,27 +82,57 @@ export async function runElectronNode(input: {
|
||||
child.stdout?.on('data', (chunk: Buffer) => { stdout = append(stdout, chunk); });
|
||||
child.stderr?.on('data', (chunk: Buffer) => { stderr = append(stderr, chunk); });
|
||||
let timedOut = false;
|
||||
let cancelled = false;
|
||||
let settled = false;
|
||||
const abort = () => {
|
||||
cancelled = true;
|
||||
void terminateTree(child);
|
||||
};
|
||||
input.signal?.addEventListener('abort', abort, { once: true });
|
||||
const timer = setTimeout(() => {
|
||||
timedOut = true;
|
||||
void terminateTree(child);
|
||||
}, input.deadlineMs);
|
||||
child.once('error', () => {
|
||||
clearTimeout(timer);
|
||||
reject(new PublishRuntimeError('LOCAL_BUILD_RUNTIME_UNAVAILABLE'));
|
||||
input.signal?.removeEventListener('abort', abort);
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
reject(new PublishRuntimeError(cancelled ? 'LOCAL_BUILD_CANCELLED' : 'LOCAL_BUILD_RUNTIME_UNAVAILABLE'));
|
||||
});
|
||||
child.once('close', (code) => {
|
||||
clearTimeout(timer);
|
||||
if (timedOut) reject(new PublishRuntimeError('LOCAL_BUILD_TIMEOUT'));
|
||||
input.signal?.removeEventListener('abort', abort);
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
if (cancelled) reject(new PublishRuntimeError('LOCAL_BUILD_CANCELLED'));
|
||||
else if (timedOut) reject(new PublishRuntimeError('LOCAL_BUILD_TIMEOUT'));
|
||||
else if (code !== 0) reject(new PublishRuntimeError('LOCAL_BUILD_FAILED'));
|
||||
else resolve({ stdout, stderr });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function resolvePublishRuntime(): Promise<PublishRuntime> {
|
||||
export async function resolvePublishRuntime(context?: Partial<PublishRuntimeContext>): Promise<PublishRuntime> {
|
||||
const isPackaged = context?.isPackaged ?? Boolean(app?.isPackaged);
|
||||
const resourcesPath = context?.resourcesPath ?? process.resourcesPath;
|
||||
const cacheKey = `${isPackaged ? 'packaged' : 'development'}:${resourcesPath}`;
|
||||
const cachedPublishRuntime = cachedPublishRuntimes.get(cacheKey);
|
||||
if (cachedPublishRuntime) return cachedPublishRuntime;
|
||||
const nextRuntime = resolvePublishRuntimeUncached({ isPackaged, resourcesPath });
|
||||
cachedPublishRuntimes.set(cacheKey, nextRuntime);
|
||||
try {
|
||||
const packagedNpmPackage = join(process.resourcesPath, 'publish-runtime', 'package.json');
|
||||
const npmPackage = app.isPackaged && existsSync(packagedNpmPackage)
|
||||
return await nextRuntime;
|
||||
} catch (error) {
|
||||
cachedPublishRuntimes.delete(cacheKey);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function resolvePublishRuntimeUncached(context: PublishRuntimeContext): Promise<PublishRuntime> {
|
||||
try {
|
||||
const packagedNpmPackage = join(context.resourcesPath, 'publish-runtime', 'package.json');
|
||||
const npmPackage = context.isPackaged && existsSync(packagedNpmPackage)
|
||||
? packagedNpmPackage
|
||||
: require.resolve('npm/package.json');
|
||||
const npmCli = join(dirname(npmPackage), 'bin', 'npm-cli.js');
|
||||
|
||||
Reference in New Issue
Block a user