feat(aws-lambda): support plan protocol v2 (#2789)

* feat(aws-lambda): support plan protocol v2

* fix(aws-lambda): align SAM v2 terminal errors
This commit is contained in:
James Russo
2026-07-25 23:42:51 -04:00
committed by GitHub
parent c6fdd9c015
commit 5bf61d6df0
46 changed files with 4687 additions and 114 deletions
@@ -201,6 +201,30 @@ describe("getRenderProgress", () => {
expect(progress.endedAt).not.toBeNull();
});
it("recognizes v2 chunk and assemble state names", async () => {
const sfn = new FakeSFN();
sfn.historyPages = [
[
stateEntered("PlanV2"),
lambdaSucceeded({ Action: "plan", TotalFrames: 30 }),
stateEntered("RenderChunkV2"),
lambdaSucceeded({ Action: "renderChunk", FramesEncoded: 30 }),
stateEntered("AssembleV2"),
lambdaSucceeded({ Action: "assemble", FramesEncoded: 30 }),
stateExited("AssembleV2", {
Output: { OutputS3Uri: "s3://b/v2.mp4", FileSize: 321 },
}),
],
];
const progress = await getRenderProgress({
executionArn: "arn",
sfn: sfn as unknown as SFNClient,
});
expect(progress.framesRendered).toBe(30);
expect(progress.overallProgress).toBe(1);
expect(progress.outputFile).toEqual({ s3Uri: "s3://b/v2.mp4", bytes: 321 });
});
it("computes cost from observed billed duration", async () => {
const sfn = new FakeSFN();
sfn.historyPages = [[lambdaSucceeded({ Action: "plan", TotalFrames: 30, DurationMs: 6_000 })]];
@@ -251,7 +251,7 @@ function summarizeHistory(events: HistoryEvent[], memoryMb: number): HistorySumm
// ResultSelector pulls FileSize + OutputS3Uri from the Lambda
// result, so we re-extract them here from the state exit's
// own output rather than relying on the Lambda payload.
if (ev.stateExitedEventDetails?.name === "Assemble") {
if (isAssembleState(ev.stateExitedEventDetails?.name)) {
assembleComplete = true;
const exitPayload = parseJson(ev.stateExitedEventDetails?.output);
if (exitPayload && typeof exitPayload === "object") {
@@ -350,12 +350,20 @@ function applyPayloadFrameCounts(
currentLambdaState: string | null,
bump: (delta: number) => void,
): void {
if (currentLambdaState !== "RenderChunk") return;
if (!isRenderChunkState(currentLambdaState)) return;
if (!payload || typeof payload !== "object") return;
const obj = payload as Record<string, unknown>;
if (typeof obj.FramesEncoded === "number") bump(obj.FramesEncoded);
}
function isRenderChunkState(name: string | null | undefined): boolean {
return name === "RenderChunk" || name === "RenderChunkV2";
}
function isAssembleState(name: string | null | undefined): boolean {
return name === "Assemble" || name === "AssembleV2";
}
/**
* Lambda success payloads from our handler include `DurationMs` — the
* wall-clock the handler observed. We use it as a best-effort proxy
@@ -89,9 +89,27 @@ describe("renderToLambda", () => {
PlanOutputS3Prefix: "s3://test-bucket/renders/smoke-1/",
OutputS3Uri: "s3://test-bucket/renders/smoke-1/output.mp4",
Config: baseConfig,
PlanProtocol: "v1",
});
});
it("opts the complete execution into plan protocol v2 explicitly", async () => {
const sfn = new FakeSFN();
const s3 = new FakeS3();
await renderToLambda({
projectDir,
bucketName: "test-bucket",
stateMachineArn: "arn:aws:states:us-east-1:1234:stateMachine:hf",
config: baseConfig,
executionName: "smoke-v2",
planProtocol: "v2",
sfn: asSFNClient(sfn),
s3: asS3Client(s3),
});
expect(sfn.starts[0]?.input).toMatchObject({ PlanProtocol: "v2" });
});
it("derives the file extension from config.format", async () => {
const sfn = new FakeSFN();
const s3 = new FakeS3();
@@ -20,7 +20,7 @@
import { randomUUID } from "node:crypto";
import { SFNClient, StartExecutionCommand } from "@aws-sdk/client-sfn";
import type { S3Client } from "@aws-sdk/client-s3";
import type { SerializableDistributedRenderConfig } from "../events.js";
import type { LambdaPlanProtocol, SerializableDistributedRenderConfig } from "../events.js";
import { formatExtension } from "../formatExtension.js";
import { formatS3Uri } from "../s3Transport.js";
import { deploySite, type SiteHandle } from "./deploySite.js";
@@ -37,6 +37,11 @@ export interface RenderToLambdaOptions {
siteHandle?: SiteHandle;
/** Validated `SerializableDistributedRenderConfig` (no logger / abortSignal). */
config: SerializableDistributedRenderConfig;
/**
* Distributed plan transport. Defaults to `"v1"` for backwards
* compatibility; v2 is always an explicit whole-render opt-in.
*/
planProtocol?: LambdaPlanProtocol;
/** S3 bucket from the SAM stack output (`RenderBucketName`). */
bucketName: string;
/** State machine ARN from the SAM stack output (`RenderStateMachineArn`). */
@@ -110,6 +115,7 @@ export async function renderToLambda(opts: RenderToLambdaOptions): Promise<Rende
PlanOutputS3Prefix: planOutputS3Prefix,
OutputS3Uri: outputS3Uri,
Config: opts.config,
PlanProtocol: opts.planProtocol ?? "v1",
};
// Reject oversize input client-side. Step Functions Standard caps the