import { HOST_EVENT_KEYS, toHostEventName } from "./constants.js"; import { type CommandSettledDetail, isCommandSettledDetail, } from "./host-event-contract.js"; type WaitForCommandSettlementInput = { requestId: string; windowRef?: Window; timeoutMs?: number; }; const DEFAULT_SETTLEMENT_TIMEOUT_MS = 30_000; export const COMMAND_SETTLEMENT_TIMEOUT_CODE = "COMMAND_SETTLEMENT_TIMEOUT" as const; export class CommandSettlementError extends Error { readonly code?: string; readonly requestId: string; readonly kind: string; readonly instanceId: string | null; constructor(input: { requestId: string; kind: string; instanceId: string | null; message: string; code?: string; }) { super(input.message); this.name = "CommandSettlementError"; this.requestId = input.requestId; this.kind = input.kind; this.instanceId = input.instanceId; if (input.code) { this.code = input.code; } } } type CommandSettledFailureDetail = Extract; export const toCommandSettlementError = ( detail: CommandSettledFailureDetail, ): CommandSettlementError => { return new CommandSettlementError({ requestId: detail.requestId, kind: detail.kind, instanceId: detail.instanceId, message: detail.error.message ?? `Command ${detail.kind} failed without an error payload`, code: detail.error.code, }); }; export class CommandSettlementTimeoutError extends Error { readonly code = COMMAND_SETTLEMENT_TIMEOUT_CODE; readonly requestId: string; constructor(requestId: string) { super(`Timed out waiting for command settlement: ${requestId}`); this.name = "CommandSettlementTimeoutError"; this.requestId = requestId; } } const eventHasDetail = (event: Event): event is Event & { detail: unknown } => typeof event === "object" && event !== null && "detail" in event; export const waitForCommandSettlement = ( input: WaitForCommandSettlementInput, ): Promise => { const requestId = input.requestId; if (!requestId) { return Promise.reject(new Error("requestId must be a non-empty string")); } const targetWindow = input.windowRef ?? (typeof window === "undefined" ? undefined : window); if (!targetWindow) { return Promise.reject( new Error("Cannot await command settlement without window"), ); } const eventName = toHostEventName(HOST_EVENT_KEYS.INSTANCE_COMMAND_SETTLED); const timeoutMs = Math.max( 0, input.timeoutMs ?? DEFAULT_SETTLEMENT_TIMEOUT_MS, ); return new Promise((resolve, reject) => { const cleanup = (): void => { if (typeof targetWindow.removeEventListener === "function") { targetWindow.removeEventListener(eventName, handleEvent); } }; const timeoutId = setTimeout(() => { cleanup(); reject(new CommandSettlementTimeoutError(requestId)); }, timeoutMs); const handleEvent = (event: Event): void => { if (!eventHasDetail(event)) { return; } const raw = event.detail; if (!isCommandSettledDetail(raw) || raw.requestId !== requestId) { return; } const detail = raw; cleanup(); clearTimeout(timeoutId); if (detail.ok) { resolve(detail); return; } reject(toCommandSettlementError(detail)); }; targetWindow.addEventListener(eventName, handleEvent); }); };