feat: expand intelligent investigation workspace
This commit is contained in:
@@ -7,13 +7,21 @@ import {
|
||||
} from ".";
|
||||
|
||||
type ExecuteAction = (request: FrontendActionRequest, signal: AbortSignal) => Promise<unknown>;
|
||||
type SubmitResult = (sessionId: string, actionId: string, result: FrontendActionResult) => Promise<void>;
|
||||
type SubmitResult = (
|
||||
sessionId: string,
|
||||
actionId: string,
|
||||
result: FrontendActionResult
|
||||
) => Promise<void>;
|
||||
const ERROR_CODE_PATTERN = /^[A-Z][A-Z0-9_]+$/;
|
||||
|
||||
export class FrontendActionExecutor {
|
||||
private readonly controllers = new Map<string, AbortController>();
|
||||
private queue: Promise<void> = Promise.resolve();
|
||||
|
||||
constructor(private readonly execute: ExecuteAction, private readonly submit: SubmitResult) {}
|
||||
constructor(
|
||||
private readonly execute: ExecuteAction,
|
||||
private readonly submit: SubmitResult
|
||||
) {}
|
||||
|
||||
async handle(value: unknown, activeSessionId: string | null): Promise<void> {
|
||||
const request = parseFrontendActionRequest(value);
|
||||
@@ -24,32 +32,90 @@ export class FrontendActionExecutor {
|
||||
return;
|
||||
}
|
||||
if (this.controllers.has(request.actionId)) return;
|
||||
const result = await this.executeOnce(request);
|
||||
storeActionResult(request.sessionId, result);
|
||||
await this.submit(request.sessionId, request.actionId, result);
|
||||
const controller = new AbortController();
|
||||
this.controllers.set(request.actionId, controller);
|
||||
const pending = this.queue.then(async () => {
|
||||
const result = await this.executeOnce(request, controller);
|
||||
storeActionResult(request.sessionId, result);
|
||||
await this.submit(request.sessionId, request.actionId, result);
|
||||
});
|
||||
this.queue = pending.catch(() => undefined);
|
||||
return pending;
|
||||
}
|
||||
|
||||
cancelAll(): void {
|
||||
for (const controller of this.controllers.values()) controller.abort();
|
||||
this.controllers.clear();
|
||||
this.queue = Promise.resolve();
|
||||
}
|
||||
|
||||
private async executeOnce(request: FrontendActionRequest): Promise<FrontendActionResult> {
|
||||
if (Date.now() >= request.expiresAt) return this.failure(request, "expired", "ACTION_EXPIRED", "frontend action expired before execution");
|
||||
const controller = new AbortController();
|
||||
this.controllers.set(request.actionId, controller);
|
||||
private async executeOnce(
|
||||
request: FrontendActionRequest,
|
||||
controller: AbortController
|
||||
): Promise<FrontendActionResult> {
|
||||
const remaining = request.expiresAt - Date.now();
|
||||
const timer =
|
||||
remaining > 0
|
||||
? setTimeout(
|
||||
() => controller.abort(new DOMException("Action expired", "TimeoutError")),
|
||||
remaining
|
||||
)
|
||||
: undefined;
|
||||
try {
|
||||
return { version: "frontend-action-result@1", actionId: request.actionId, status: "succeeded", output: await this.execute(request, controller.signal), completedAt: Date.now() };
|
||||
controller.signal.throwIfAborted();
|
||||
if (remaining <= 0)
|
||||
return this.failure(
|
||||
request,
|
||||
"expired",
|
||||
"ACTION_EXPIRED",
|
||||
"frontend action expired before execution"
|
||||
);
|
||||
const output = await this.execute(request, controller.signal);
|
||||
controller.signal.throwIfAborted();
|
||||
return {
|
||||
version: "frontend-action-result@2",
|
||||
actionId: request.actionId,
|
||||
status: "succeeded",
|
||||
output,
|
||||
completedAt: Date.now()
|
||||
};
|
||||
} catch (error) {
|
||||
const expired =
|
||||
controller.signal.reason instanceof DOMException &&
|
||||
controller.signal.reason.name === "TimeoutError";
|
||||
const message = error instanceof Error ? error.message : "frontend action failed";
|
||||
const code = controller.signal.aborted ? "ACTION_CANCELLED" : ERROR_CODE_PATTERN.test(message) ? message : "ACTION_FAILED";
|
||||
return this.failure(request, controller.signal.aborted ? "cancelled" : "failed", code, message);
|
||||
const code = expired
|
||||
? "ACTION_EXPIRED"
|
||||
: controller.signal.aborted
|
||||
? "ACTION_CANCELLED"
|
||||
: ERROR_CODE_PATTERN.test(message)
|
||||
? message
|
||||
: "ACTION_FAILED";
|
||||
return this.failure(
|
||||
request,
|
||||
expired ? "expired" : controller.signal.aborted ? "cancelled" : "failed",
|
||||
code,
|
||||
message
|
||||
);
|
||||
} finally {
|
||||
this.controllers.delete(request.actionId);
|
||||
clearTimeout(timer);
|
||||
if (this.controllers.get(request.actionId) === controller)
|
||||
this.controllers.delete(request.actionId);
|
||||
}
|
||||
}
|
||||
|
||||
private failure(request: FrontendActionRequest, status: "failed" | "cancelled" | "expired", code: string, message: string): FrontendActionResult {
|
||||
return { version: "frontend-action-result@1", actionId: request.actionId, status, error: { code, message }, completedAt: Date.now() };
|
||||
private failure(
|
||||
request: FrontendActionRequest,
|
||||
status: "failed" | "cancelled" | "expired",
|
||||
code: string,
|
||||
message: string
|
||||
): FrontendActionResult {
|
||||
return {
|
||||
version: "frontend-action-result@2",
|
||||
actionId: request.actionId,
|
||||
status,
|
||||
error: { code, message },
|
||||
completedAt: Date.now()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user