import { parseFrontendActionRequest, readActionResults, storeActionResult, type FrontendActionRequest, type FrontendActionResult } from "."; type ExecuteAction = (request: FrontendActionRequest, signal: AbortSignal) => Promise; type SubmitResult = ( sessionId: string, actionId: string, result: FrontendActionResult ) => Promise; const ERROR_CODE_PATTERN = /^[A-Z][A-Z0-9_]+$/; export class FrontendActionExecutor { private readonly controllers = new Map(); private queue: Promise = Promise.resolve(); constructor( private readonly execute: ExecuteAction, private readonly submit: SubmitResult ) {} async handle(value: unknown, activeSessionId: string | null): Promise { const request = parseFrontendActionRequest(value); if (!request || request.sessionId !== activeSessionId) return; const saved = readActionResults(request.sessionId)[request.actionId]; if (saved) { await this.submit(request.sessionId, request.actionId, saved); return; } if (this.controllers.has(request.actionId)) return; 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, controller: AbortController ): Promise { const remaining = request.expiresAt - Date.now(); const timer = remaining > 0 ? setTimeout( () => controller.abort(new DOMException("Action expired", "TimeoutError")), remaining ) : undefined; try { 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 = 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 { 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@2", actionId: request.actionId, status, error: { code, message }, completedAt: Date.now() }; } }