mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-07 10:06:21 +00:00
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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user