mirror of
https://github.com/morluto/rea.git
synced 2026-11-03 13:26:10 +00:00
110 lines
3.7 KiB
TypeScript
110 lines
3.7 KiB
TypeScript
import type { AnalysisPartialObservation } from "../domain/analysisErrorBase.js";
|
|
import {
|
|
AnalysisCancelledError,
|
|
AnalysisTimeoutError,
|
|
} from "../domain/analysisErrorCore.js";
|
|
import {
|
|
DEFAULT_NATIVE_UI_DEADLINE_MS,
|
|
nativeDeadlineRecovery,
|
|
} from "../domain/native/nativeDeadlines.js";
|
|
|
|
type Interruption =
|
|
| { readonly kind: "caller"; readonly reason: unknown }
|
|
| { readonly kind: "deadline"; readonly reason: unknown };
|
|
|
|
/** Tracks whether the caller or the owned operation deadline stopped UI work. */
|
|
export class NativeUiInterruptionControl {
|
|
readonly signal: AbortSignal;
|
|
#controller = new AbortController();
|
|
#interruption: Interruption | undefined;
|
|
#caller: AbortSignal | undefined;
|
|
#deadline: AbortSignal;
|
|
#deadlineMs: number;
|
|
#onCallerAbort = (): void => {
|
|
this.#interrupt("caller", this.#caller?.reason);
|
|
};
|
|
#onDeadlineAbort = (): void => {
|
|
this.#interrupt("deadline", this.#deadline.reason);
|
|
};
|
|
|
|
/** Selected `native_ui_deadline_ms`, including the omitted default. */
|
|
get deadlineMs(): number {
|
|
return this.#deadlineMs;
|
|
}
|
|
|
|
constructor(
|
|
caller: AbortSignal | undefined,
|
|
deadline: AbortSignal,
|
|
deadlineMs = DEFAULT_NATIVE_UI_DEADLINE_MS,
|
|
) {
|
|
this.#caller = caller;
|
|
this.#deadline = deadline;
|
|
this.#deadlineMs = deadlineMs;
|
|
this.signal = this.#controller.signal;
|
|
|
|
// Match AbortSignal.any's iterable priority when both are already aborted.
|
|
if (caller?.aborted) this.#interrupt("caller", caller.reason);
|
|
else if (deadline.aborted) this.#interrupt("deadline", deadline.reason);
|
|
else {
|
|
caller?.addEventListener("abort", this.#onCallerAbort, { once: true });
|
|
deadline.addEventListener("abort", this.#onDeadlineAbort, {
|
|
once: true,
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A helper subprocess reached the selected deadline before the abort signal
|
|
* settled. Caller cancellation already recorded on this control stays primary.
|
|
*/
|
|
deadlineFailure(
|
|
operation: string,
|
|
partialObservation?: AnalysisPartialObservation,
|
|
): AnalysisTimeoutError | AnalysisCancelledError {
|
|
if (this.#interruption === undefined)
|
|
this.#interrupt("deadline", this.#deadline.reason);
|
|
const error = this.error(operation, partialObservation);
|
|
if (error === undefined)
|
|
throw new Error("UI deadline failure requires an interruption.");
|
|
return error;
|
|
}
|
|
|
|
error(
|
|
operation: string,
|
|
partialObservation?: AnalysisPartialObservation,
|
|
): AnalysisCancelledError | AnalysisTimeoutError | undefined {
|
|
const interruption = this.#interruption;
|
|
if (interruption === undefined) return undefined;
|
|
return interruption.kind === "deadline"
|
|
? new AnalysisTimeoutError(operation, this.#deadlineMs, {
|
|
cause: interruption.reason,
|
|
partialObservation,
|
|
...nativeDeadlineRecovery({
|
|
timeoutMs: this.#deadlineMs,
|
|
setting: "native_ui_deadline_ms",
|
|
cliFlag: "--native-ui-deadline-ms",
|
|
defaultMs: DEFAULT_NATIVE_UI_DEADLINE_MS,
|
|
phase: "native UI observation deadline",
|
|
detail: " A partial observation may already be retained.",
|
|
unchanged:
|
|
"The 67108864-byte scenario output allowance and the 180000 ms cap on one wait step have no runtime override.",
|
|
}),
|
|
})
|
|
: new AnalysisCancelledError(operation, {
|
|
cause: interruption.reason,
|
|
partialObservation,
|
|
});
|
|
}
|
|
|
|
dispose(): void {
|
|
this.#caller?.removeEventListener("abort", this.#onCallerAbort);
|
|
this.#deadline.removeEventListener("abort", this.#onDeadlineAbort);
|
|
}
|
|
|
|
#interrupt(kind: Interruption["kind"], reason: unknown): void {
|
|
if (this.#interruption !== undefined) return;
|
|
this.#interruption = { kind, reason };
|
|
this.#controller.abort(reason);
|
|
}
|
|
}
|