mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 21:15:32 +09:00
130 lines
4.8 KiB
TypeScript
130 lines
4.8 KiB
TypeScript
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
import {REQUEST_ID_MAX_LENGTH, readBoundedString, readExactPlainRecord} from '@electron/common/PlainRecord';
|
|
import {resolveDesktopRuntimePlan} from '@electron/main/DesktopRuntimeDiscovery';
|
|
import {projectDesktopRuntimePlan} from '@electron/main/DesktopRuntimePlanProjection';
|
|
import {getDesktopLocalAppProtocol} from '@electron/main/LocalAppProtocol';
|
|
import type {RendererDocumentOwnerFactory} from '@electron/main/RendererDocumentOwner';
|
|
import type {
|
|
RendererDocumentIpcEvent,
|
|
RendererDocumentOwner,
|
|
RendererDocumentOwnerWatcher,
|
|
} from '@electron/main/RendererDocumentOwnership';
|
|
import type {DesktopRuntimePlan} from '@fluxer/desktop_ipc/src/LocalAppRuntimeContract';
|
|
|
|
const MAX_PENDING_RESOLUTIONS = 8;
|
|
const RESOLVE_INPUT_MAX_LENGTH = 8192;
|
|
|
|
class InvalidDesktopRuntimeResolutionRequestError extends TypeError {
|
|
public constructor(context: string) {
|
|
super(`Desktop runtime config ${context} is malformed`);
|
|
this.name = 'InvalidDesktopRuntimeResolutionRequestError';
|
|
}
|
|
}
|
|
|
|
class DesktopRuntimeResolutionCapacityError extends Error {
|
|
public constructor() {
|
|
super('Desktop runtime config resolution capacity is exhausted');
|
|
this.name = 'DesktopRuntimeResolutionCapacityError';
|
|
}
|
|
}
|
|
|
|
class DesktopRuntimeResolutionAlreadyExistsError extends Error {
|
|
public constructor(requestId: string) {
|
|
super(`Desktop runtime resolution ${requestId} already exists`);
|
|
this.name = 'DesktopRuntimeResolutionAlreadyExistsError';
|
|
}
|
|
}
|
|
|
|
class DesktopRuntimeResolutionOwnershipError extends Error {
|
|
public constructor(requestId: string) {
|
|
super(`Desktop runtime resolution ${requestId} belongs to another renderer document`);
|
|
this.name = 'DesktopRuntimeResolutionOwnershipError';
|
|
}
|
|
}
|
|
|
|
interface PendingRuntimeResolution {
|
|
readonly requestId: string;
|
|
readonly owner: RendererDocumentOwner;
|
|
readonly watcher: RendererDocumentOwnerWatcher;
|
|
readonly controller: AbortController;
|
|
}
|
|
|
|
export class DesktopRuntimeResolutionRegistry {
|
|
private readonly pending = new Map<string, PendingRuntimeResolution>();
|
|
|
|
public constructor(private readonly rendererDocuments: RendererDocumentOwnerFactory) {}
|
|
|
|
public async resolve(event: RendererDocumentIpcEvent, request: unknown): Promise<DesktopRuntimePlan> {
|
|
const record = readExactPlainRecord({value: request, expectedKeys: ['input', 'requestId']});
|
|
if (record == null) {
|
|
throw new InvalidDesktopRuntimeResolutionRequestError('resolve request');
|
|
}
|
|
const input = readBoundedString(record.input, RESOLVE_INPUT_MAX_LENGTH);
|
|
const requestId = readBoundedString(record.requestId, REQUEST_ID_MAX_LENGTH);
|
|
if (input == null || requestId == null) {
|
|
throw new InvalidDesktopRuntimeResolutionRequestError('resolve request');
|
|
}
|
|
if (this.pending.has(requestId)) {
|
|
throw new DesktopRuntimeResolutionAlreadyExistsError(requestId);
|
|
}
|
|
if (this.pending.size >= MAX_PENDING_RESOLUTIONS) {
|
|
throw new DesktopRuntimeResolutionCapacityError();
|
|
}
|
|
const owner = this.rendererDocuments.capture(event, 'Desktop runtime config resolve');
|
|
const controller = new AbortController();
|
|
const watcher = owner.watchInvalidation(() => {
|
|
this.dispose(requestId);
|
|
});
|
|
const pending = {requestId, owner, watcher, controller};
|
|
this.pending.set(requestId, pending);
|
|
try {
|
|
const protocol = getDesktopLocalAppProtocol();
|
|
const signal = AbortSignal.any([controller.signal, protocol.getShutdownSignal()]);
|
|
const plan = await resolveDesktopRuntimePlan({input, signal});
|
|
owner.requireCurrent('Desktop runtime config resolve');
|
|
protocol.cacheRuntimePlan(plan);
|
|
return projectDesktopRuntimePlan(plan);
|
|
} finally {
|
|
this.dispose(requestId, pending);
|
|
}
|
|
}
|
|
|
|
public cancel(event: RendererDocumentIpcEvent, request: unknown): void {
|
|
const requestId = requireResolutionId(request);
|
|
const pending = this.pending.get(requestId);
|
|
if (pending === undefined) {
|
|
return;
|
|
}
|
|
if (!pending.owner.matchesEvent(event)) {
|
|
throw new DesktopRuntimeResolutionOwnershipError(requestId);
|
|
}
|
|
this.dispose(requestId, pending);
|
|
}
|
|
|
|
public cleanup(): void {
|
|
for (const requestId of [...this.pending.keys()]) {
|
|
this.dispose(requestId);
|
|
}
|
|
}
|
|
|
|
private dispose(requestId: string, expected?: PendingRuntimeResolution): void {
|
|
const pending = this.pending.get(requestId);
|
|
if (pending === undefined || (expected !== undefined && pending !== expected)) {
|
|
return;
|
|
}
|
|
this.pending.delete(requestId);
|
|
pending.controller.abort();
|
|
pending.watcher.dispose();
|
|
}
|
|
}
|
|
|
|
function requireResolutionId(request: unknown): string {
|
|
const record = readExactPlainRecord({value: request, expectedKeys: ['requestId']});
|
|
const requestId = record == null ? null : readBoundedString(record.requestId, REQUEST_ID_MAX_LENGTH);
|
|
if (requestId === null) {
|
|
throw new InvalidDesktopRuntimeResolutionRequestError('resolution cancellation request');
|
|
}
|
|
return requestId;
|
|
}
|