import type { ResourceEvent } from "@fjall/util/aws"; import { type EventLogWriter, type EventFailureAnalyser, type AwsClientLike, type StackEventsPageRead, type StackResourceStatusRead, type StackStatusRead } from "./cloudformationEventTypes.js"; import type { FailureAnalysis } from "@fjall/util/aws"; export declare function isTerminalState(status: string): boolean; export declare function isSuccessState(status: string): boolean; export declare function isIpamResource(resourceType: string): boolean; /** Tracks in-progress IPAM resources for concurrency diagnostics */ export declare class IpamConcurrencyTracker { private inProgress; get size(): number; clear(): void; /** * Process an IPAM resource event — logs concurrency info and writes * diagnostic metadata to the event logger when provided. */ trackEvent(event: ResourceEvent, eventLogger: EventLogWriter | null): void; } /** * Clock-skew allowance subtracted from the monitor start time to form the * event cutoff (F-F8). CloudFormation event timestamps come from AWS clocks; * two minutes comfortably covers client/AWS drift (SigV4 itself tolerates * five) without re-admitting a previous operation's history. * * Accepted residual (F-R11, narrowed by F-X9): after a FAILED prime poll, a * previous operation's events inside the skew window (terminal within 120s * of startMonitoring, same stack) are admitted for DISPATCH and terminal * detection. Tightening those to an unskewed `>= monitorStart` bound would * fail CLOSED under a fast client clock (this operation's own terminal event * skipped → event-driven completion silently disabled), and an * AWS-clock-only discriminator (terminal newer than the newest observed * stack-level IN_PROGRESS) hangs any monitor attached to an * already-in-flight operation. That residual is bounded: the completion * verdict is re-read from live DescribeStacks in handleStackCompletion and * the deploy result derives from the CDK subprocess lifecycle. * * The ClientRequestToken is NOT part of the residual (F-X9): the latched * token becomes the webapp reconciler's §5.4 adoption comparand (heartbeat → * Deployment.clientRequestToken → matchesToken/deriveAdoption), where a * previous operation's token would corroborate the WRONG operation's * outcome. Skew-window events are therefore dispatched with the token * withheld (`tokenLatchCutoffMs`, unskewed) — an absent comparand merely * skips adoption assist, and CFN's own ClientRequestToken remains the §5.3 * fencing backstop. */ export declare const EVENT_CUTOFF_SKEW_MS = 120000; /** * The one operation a monitor follows (FJL-1015): the request token it was * sent with, and whether the poll has read its first event yet. Only the * monitor that made it mutates `started`. */ export interface MonitoredOperation { readonly clientRequestToken: string; started: boolean; } /** Mutable state passed into pollStackEvents from the monitor class */ export interface PollContext { aws: AwsClientLike; seenEventIds: Set; activeNestedStacks: Map; /** Each resource's latest failure reason, for the event log's summary. */ failureReasons: Map; eventHistory: Map; maxHistorySize: number; ipamTracker: IpamConcurrencyTracker; eventLogger: EventLogWriter | null; /** * Epoch ms below which events belong to a PREVIOUS operation and are * absorbed (marked seen, never processed or dispatched). Guards the case * where the prime poll fails (throttle/network), leaving seenEventIds * empty so the first live poll walks the stack's full prior history (F-F8). */ eventCutoffMs: number; /** * Epoch ms of startMonitoring, UNSKEWED (F-X9). Events at/after this bound * carry their ClientRequestToken; events inside the skew window * (eventCutoffMs ≤ t < tokenLatchCutoffMs) are dispatched with the token * withheld — the token feeds the tracker's §5.9 latch and thence the * reconciler's adoption comparand, where a previous operation's token * corroborates the wrong operation. Dispatch and terminal detection keep * the skewed cutoff. */ tokenLatchCutoffMs: number; /** * When set, the poll reads that operation alone. Until an event carries its * request token, the operation's own start, every event is absorbed: never * dispatched, recorded, or read as terminal. From that event on, every * event is the operation's, its rollback's included, since CloudFormation * runs one operation on a stack at a time. A rollback a cancel began * carries the cancel's token, or none for a cancel sent without one (read * in production us-east-1's WebAppCompute events on 2026-10-04), so the * token is compared only to find the start. */ operation?: MonitoredOperation; } /** * Poll CloudFormation for new stack events and dispatch them to the callback, * oldest first. DescribeStackEvents pages newest first, so the walk collects * every new event before it handles any. A terminal stack event then reaches * the callback after the failures that led to it, the history and the * nested-stack map end on each resource's latest status, and a walk that * throws part-way marks nothing it collected as seen, so the next poll reads * those events again. */ export declare function pollStackEvents(ctx: PollContext, stackName: string, onResourceUpdate: (event: ResourceEvent) => void): Promise; /** * The reason of the failure that failed the stack's current operation (see * `findRootFailure`), or undefined when the history holds no failure. */ export declare function rootFailureReason(eventHistory: ReadonlyMap, stackName: string): string | undefined; /** Result of handling stack completion */ export interface StackCompletionResult { success: boolean; failureMessage?: string; analysis: FailureAnalysis | null; } /** * Handle stack completion — determines success/failure, logs results, * and runs failure analysis. Called after pollEvents detects a terminal state. */ export declare function handleStackCompletion(aws: AwsClientLike, stackName: string, failureReasons: Map, eventLogger: EventLogWriter | null, failureAnalyser: EventFailureAnalyser | null, eventHistory: Map): Promise; /** Fetch the current status of a CloudFormation stack */ export declare function fetchStackStatus(aws: AwsClientLike, stackName: string, abortSignal?: AbortSignal): Promise<{ status: string; statusReason?: string; } | null>; /** * CloudFormation's own "no such stack" answer: a `ValidationError` whose * message says the stack does not exist. Anything else carrying the same * words (a throttle that echoes the request, a query-protocol wrapper) is * not proof of absence and must not read as one. */ export declare function isStackNotFoundError(error: unknown): boolean; /** A status read this module cannot place; the `never` makes a new kind a compile error. */ export declare function unknownStackStatusRead(read: never): string; /** * A status read that failed, as `unreadable`. The reason is pre-masked at * construction, by `sdkFailureReason`: a per-call timeout is named by its * budget, never by the SDK's `AbortError` text, which reads as the * operator's cancel, and any other failure is * `maskSensitiveOutput(getErrorMessage(error))`. A throttle * (`THROTTLING_ERROR_NAMES`), a timeout and a network fault are marked worth * asking again, as main's `classifySdkCallError` marks them; every other * failure is not, a read the caller cancelled included. */ export declare function unreadableRead(error: unknown, abortSignal: AbortSignal | undefined): { readonly kind: "unreadable"; readonly reason: string; readonly recoverable: boolean; }; /** * Read a stack's status with one DescribeStacks. Absence is reported only on * proof: an empty `Stacks` list, or CloudFormation's own not-found * `ValidationError` (`isStackNotFoundError`). Any other failure is * `unreadable`, and so are an answer with no `Stacks` list and a stack with * no status. * * An `unreadable` reason is pre-masked at construction: an SDK failure's is * worded by `unreadableRead`, which names a per-call timeout by its budget * and masks any other as `maskSensitiveOutput(getErrorMessage(error))`, and * the two other reasons are fixed literals. * * `fetchStackStatus` keeps its two-valued answer, which reads every failure * as no stack, because `CloudFormationEventMonitor.getStackStatus` is * published with that shape. A caller that must tell a missing stack from a * read that failed asks this one. */ export declare function fetchStackStatusRead(aws: AwsClientLike, stackName: string, abortSignal?: AbortSignal): Promise; /** * Read one resource's status with one DescribeStackResource. Absence is * reported only on CloudFormation's own not-found `ValidationError`, for the * stack or the resource (`isStackNotFoundError`). Any other failure is * `unreadable`, and so is an answer with no status or no time it last * changed. * * An `unreadable` reason is pre-masked at construction, as * `fetchStackStatusRead`'s is: an SDK failure's is worded by * `unreadableRead`, and the other reason is a fixed literal. */ export declare function fetchStackResourceStatus(aws: AwsClientLike, stackName: string, logicalId: string, abortSignal?: AbortSignal): Promise; /** * Read the stack's newest events with one DescribeStackEvents: the first * page, newest first. Absence is reported only on CloudFormation's own * not-found `ValidationError` (`isStackNotFoundError`). Any other failure is * `unreadable`, and so is an answer with no event list: an answer that leaves * the list out has not said the stack has no events. * * An `unreadable` reason is pre-masked at construction, as * `fetchStackStatusRead`'s is: an SDK failure's is worded by * `unreadableRead`, and the other reason is a fixed literal. */ export declare function fetchNewestStackEvents(aws: AwsClientLike, stackName: string, abortSignal?: AbortSignal): Promise; /** Fetch the latest event for each resource in a stack */ export declare function fetchCurrentResources(aws: AwsClientLike, stackName: string, abortSignal?: AbortSignal): Promise;