perf: optimize app startup and background lifecycles
This commit is contained in:
169
electron/api/host-api-dispatcher.ts
Normal file
169
electron/api/host-api-dispatcher.ts
Normal file
@@ -0,0 +1,169 @@
|
||||
import { Readable, Writable } from 'node:stream';
|
||||
import type { IncomingHttpHeaders, IncomingMessage, ServerResponse } from 'node:http';
|
||||
import type { HostApiContext } from './context';
|
||||
import { hostApiRouteHandlers } from './route-handlers';
|
||||
import { requireJsonContentType, sendJson } from './route-utils';
|
||||
|
||||
export type HostApiDispatchInput = {
|
||||
path: string;
|
||||
method?: string;
|
||||
headers?: Record<string, string>;
|
||||
body?: string;
|
||||
};
|
||||
|
||||
export type HostApiDispatchData = {
|
||||
status: number;
|
||||
ok: boolean;
|
||||
json?: unknown;
|
||||
text?: string;
|
||||
};
|
||||
|
||||
class DispatchResponse extends Writable {
|
||||
statusCode = 200;
|
||||
statusMessage?: string;
|
||||
headersSent = false;
|
||||
socket = null;
|
||||
|
||||
private readonly headers = new Map<string, string | string[]>();
|
||||
private readonly chunks: Buffer[] = [];
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
write: (chunk: Buffer | string, _encoding, callback) => {
|
||||
this.chunks.push(Buffer.isBuffer(chunk) ? Buffer.from(chunk) : Buffer.from(chunk));
|
||||
callback();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
setHeader(name: string, value: string | number | readonly string[]): this {
|
||||
this.headers.set(name.toLowerCase(), Array.isArray(value) ? [...value] : String(value));
|
||||
return this;
|
||||
}
|
||||
|
||||
getHeader(name: string): string | string[] | undefined {
|
||||
return this.headers.get(name.toLowerCase());
|
||||
}
|
||||
|
||||
hasHeader(name: string): boolean {
|
||||
return this.headers.has(name.toLowerCase());
|
||||
}
|
||||
|
||||
removeHeader(name: string): void {
|
||||
this.headers.delete(name.toLowerCase());
|
||||
}
|
||||
|
||||
writeHead(
|
||||
statusCode: number,
|
||||
statusMessageOrHeaders?: string | Record<string, string | number | readonly string[]>,
|
||||
maybeHeaders?: Record<string, string | number | readonly string[]>,
|
||||
): this {
|
||||
this.statusCode = statusCode;
|
||||
if (typeof statusMessageOrHeaders === 'string') {
|
||||
this.statusMessage = statusMessageOrHeaders;
|
||||
for (const [name, value] of Object.entries(maybeHeaders ?? {})) this.setHeader(name, value);
|
||||
} else {
|
||||
for (const [name, value] of Object.entries(statusMessageOrHeaders ?? {})) this.setHeader(name, value);
|
||||
}
|
||||
this.headersSent = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
flushHeaders(): void {
|
||||
this.headersSent = true;
|
||||
}
|
||||
|
||||
body(): Buffer {
|
||||
return Buffer.concat(this.chunks);
|
||||
}
|
||||
|
||||
contentType(): string {
|
||||
const value = this.getHeader('content-type');
|
||||
return Array.isArray(value) ? value[0] ?? '' : value ?? '';
|
||||
}
|
||||
}
|
||||
|
||||
function createRequest(input: HostApiDispatchInput): IncomingMessage {
|
||||
const body = input.body ?? '';
|
||||
const request = Readable.from(body ? [Buffer.from(body, 'utf8')] : []) as Readable & Partial<IncomingMessage>;
|
||||
const headers: IncomingHttpHeaders = Object.create(null) as IncomingHttpHeaders;
|
||||
for (const [name, value] of Object.entries(input.headers ?? {})) {
|
||||
headers[name.toLowerCase()] = value;
|
||||
}
|
||||
if (body && headers['content-length'] === undefined) {
|
||||
headers['content-length'] = String(Buffer.byteLength(body, 'utf8'));
|
||||
}
|
||||
request.method = (input.method ?? 'GET').toUpperCase();
|
||||
request.url = input.path;
|
||||
request.headers = headers;
|
||||
return request as IncomingMessage;
|
||||
}
|
||||
|
||||
function readResponseData(response: DispatchResponse): HostApiDispatchData {
|
||||
const body = response.body();
|
||||
if (response.statusCode === 204 || body.length === 0) {
|
||||
return { status: response.statusCode, ok: response.statusCode >= 200 && response.statusCode < 300 };
|
||||
}
|
||||
const text = body.toString('utf8');
|
||||
if (response.contentType().toLowerCase().includes('json')) {
|
||||
try {
|
||||
return {
|
||||
status: response.statusCode,
|
||||
ok: response.statusCode >= 200 && response.statusCode < 300,
|
||||
json: JSON.parse(text) as unknown,
|
||||
};
|
||||
} catch {
|
||||
// Keep malformed route output observable to the Renderer instead of
|
||||
// silently treating it as an empty response.
|
||||
}
|
||||
}
|
||||
return {
|
||||
status: response.statusCode,
|
||||
ok: response.statusCode >= 200 && response.statusCode < 300,
|
||||
text,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch a Renderer Host API request without a second IPC -> loopback HTTP
|
||||
* hop. This adapter deliberately reuses the existing Node route handlers so
|
||||
* authentication, validation and response contracts remain unchanged.
|
||||
*/
|
||||
export async function dispatchHostApiRequest(
|
||||
ctx: HostApiContext,
|
||||
input: HostApiDispatchInput,
|
||||
): Promise<HostApiDispatchData> {
|
||||
const request = createRequest(input);
|
||||
const response = new DispatchResponse();
|
||||
const url = new URL(input.path, 'http://127.0.0.1');
|
||||
|
||||
try {
|
||||
if (!requireJsonContentType(request)) {
|
||||
sendJson(response as unknown as ServerResponse, 415, {
|
||||
success: false,
|
||||
error: 'Content-Type must be application/json',
|
||||
});
|
||||
return readResponseData(response);
|
||||
}
|
||||
|
||||
for (const handler of hostApiRouteHandlers) {
|
||||
if (await handler(request, response as unknown as ServerResponse, url, ctx)) {
|
||||
return readResponseData(response);
|
||||
}
|
||||
}
|
||||
sendJson(response as unknown as ServerResponse, 404, {
|
||||
success: false,
|
||||
error: `No route for ${request.method} ${url.pathname}`,
|
||||
});
|
||||
return readResponseData(response);
|
||||
} catch (error) {
|
||||
if (!response.headersSent && !response.writableEnded) {
|
||||
sendJson(response as unknown as ServerResponse, 500, {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
return readResponseData(response);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user