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
@@ -26,6 +26,7 @@ import {
createWriteStream,
existsSync,
mkdirSync,
readdirSync,
readFileSync,
statSync,
writeFileSync,
@@ -37,7 +38,6 @@ import { downloadS3ObjectToFile, tarDirectory, untarDirectory } from "@hyperfram
import { handler } from "@hyperframes/aws-lambda/handler";
import type {
AssembleEvent,
AssembleLambdaResult,
HandlerDeps,
PlanEvent,
PlanLambdaResult,
@@ -46,8 +46,14 @@ import type {
SerializableDistributedRenderConfig,
} from "@hyperframes/aws-lambda";
export type { RunLambdaLocalInput } from "./regression-harness-lambda-local-types.js";
import type { RunLambdaLocalInput } from "./regression-harness-lambda-local-types.js";
export type {
LambdaLocalRenderResult,
RunLambdaLocalInput,
} from "./regression-harness-lambda-local-types.js";
import type {
LambdaLocalRenderResult,
RunLambdaLocalInput,
} from "./regression-harness-lambda-local-types.js";
const FAKE_BUCKET = "harness-lambda-local";
@@ -60,7 +66,10 @@ function uri(key: string): string {
* Run plan → renderChunk × N → assemble through the OSS handler with a
* filesystem-backed fake S3. Output lands at `input.renderedOutputPath`.
*/
export async function runLambdaLocalRender(input: RunLambdaLocalInput): Promise<void> {
export async function runLambdaLocalRender(
input: RunLambdaLocalInput,
): Promise<LambdaLocalRenderResult> {
const protocol = input.protocol ?? "v1";
const s3Root = join(input.tempRoot, "s3");
mkdirSync(s3Root, { recursive: true });
@@ -81,7 +90,26 @@ export async function runLambdaLocalRender(input: RunLambdaLocalInput): Promise<
skipChromeResolution: true,
tmpRoot: join(input.tempRoot, "lambda-tmp"),
};
mkdirSync(deps.tmpRoot as string, { recursive: true });
const lambdaTmpRoot = join(input.tempRoot, "lambda-tmp");
mkdirSync(lambdaTmpRoot, { recursive: true });
let peakObservedMaterializedBytes = measureDirectoryBytes(lambdaTmpRoot);
const observe = async <Result>(operation: () => Promise<Result>): Promise<Result> => {
const sample = (): void => {
peakObservedMaterializedBytes = Math.max(
peakObservedMaterializedBytes,
measureDirectoryBytes(lambdaTmpRoot),
);
};
sample();
const timer = setInterval(sample, 2);
timer.unref();
try {
return await operation();
} finally {
clearInterval(timer);
sample();
}
};
const config: SerializableDistributedRenderConfig = {
fps: input.fps,
@@ -91,6 +119,7 @@ export async function runLambdaLocalRender(input: RunLambdaLocalInput): Promise<
...(input.format === "mp4" && input.codec !== undefined ? { codec: input.codec } : {}),
chunkSize: input.chunkSize,
maxParallelChunks: input.maxParallelChunks,
planDirSizeLimitBytes: input.planDirSizeLimitBytes,
hdrMode: "force-sdr",
// Forward `variables` through the event boundary so lambda-local mode
// exercises the same variables-in-encoder.json path that real Lambda
@@ -101,42 +130,95 @@ export async function runLambdaLocalRender(input: RunLambdaLocalInput): Promise<
// STEP A: plan
const planPrefix = `renders/harness/${Date.now()}/`;
const planEvent: PlanEvent = {
Action: "plan",
ProjectS3Uri: uri(projectKey),
PlanOutputS3Prefix: uri(planPrefix),
Config: config,
};
const planResult = (await handler(planEvent, deps)) as PlanLambdaResult;
const planEvent: PlanEvent =
protocol === "v2"
? {
Action: "plan",
PlanProtocol: "v2",
ProjectS3Uri: uri(projectKey),
PlanOutputS3Prefix: uri(planPrefix),
Config: config,
}
: {
Action: "plan",
PlanProtocol: "v1",
ProjectS3Uri: uri(projectKey),
PlanOutputS3Prefix: uri(planPrefix),
Config: config,
};
const planResponse = await observe(() => handler(planEvent, deps));
if (planResponse.Action !== "plan") {
throw new Error(`lambda-local: plan action returned ${planResponse.Action}`);
}
const planResult: PlanLambdaResult = planResponse;
// STEP B: render every chunk through the handler.
const chunkUris: string[] = [];
const chunks: LambdaLocalRenderResult["chunks"] = [];
for (let i = 0; i < planResult.ChunkCount; i++) {
const chunkEvent: RenderChunkEvent = {
Action: "renderChunk",
PlanS3Uri: planResult.PlanS3Uri,
const shared = {
Action: "renderChunk" as const,
PlanHash: planResult.PlanHash,
ChunkIndex: i,
ChunkOutputS3Prefix: uri(planPrefix),
Format: input.format,
};
const chunkResult = (await handler(chunkEvent, deps)) as RenderChunkLambdaResult;
const chunkEvent: RenderChunkEvent =
protocol === "v2"
? {
...shared,
PlanProtocol: "v2",
PlanV2ManifestS3Uri: requireV2PlanResult(planResult).PlanV2ManifestS3Uri,
PlanV2ArtifactS3Prefix: requireV2PlanResult(planResult).PlanV2ArtifactS3Prefix,
}
: {
...shared,
PlanProtocol: "v1",
PlanS3Uri: requireV1PlanResult(planResult).PlanS3Uri,
};
const chunkResponse = await observe(() => handler(chunkEvent, deps));
if (chunkResponse.Action !== "renderChunk") {
throw new Error(`lambda-local: renderChunk action returned ${chunkResponse.Action}`);
}
const chunkResult: RenderChunkLambdaResult = chunkResponse;
chunkUris.push(chunkResult.ChunkS3Uri);
chunks.push({
index: i,
path: fakeS3Path(s3Root, chunkResult.ChunkS3Uri),
reportedSha256: chunkResult.Sha256,
});
}
// STEP C: assemble
const finalUri = uri(
`${planPrefix}output${input.format === "png-sequence" ? ".tar.gz" : `.${input.format}`}`,
);
const assembleEvent: AssembleEvent = {
Action: "assemble",
PlanS3Uri: planResult.PlanS3Uri,
const assembleShared = {
Action: "assemble" as const,
ChunkS3Uris: chunkUris,
AudioS3Uri: planResult.AudioS3Uri,
OutputS3Uri: finalUri,
Format: input.format,
};
(await handler(assembleEvent, deps)) as AssembleLambdaResult;
const assembleEvent: AssembleEvent =
protocol === "v2"
? {
...assembleShared,
PlanProtocol: "v2",
PlanV2ManifestS3Uri: requireV2PlanResult(planResult).PlanV2ManifestS3Uri,
PlanV2ArtifactS3Prefix: requireV2PlanResult(planResult).PlanV2ArtifactS3Prefix,
PlanHash: planResult.PlanHash,
}
: {
...assembleShared,
PlanProtocol: "v1",
PlanS3Uri: requireV1PlanResult(planResult).PlanS3Uri,
};
const assembleResponse = await observe(() => handler(assembleEvent, deps));
if (assembleResponse.Action !== "assemble") {
throw new Error(`lambda-local: assemble action returned ${assembleResponse.Action}`);
}
const transferBytes = fakeS3.transferBytes;
// Copy the final output from fake-S3 land back out to the path the
// harness expects. For png-sequence, untar into the dir.
@@ -152,6 +234,57 @@ export async function runLambdaLocalRender(input: RunLambdaLocalInput): Promise<
input.renderedOutputPath,
);
}
return {
protocol,
outputPath: input.renderedOutputPath,
chunks,
transferBytes,
peakMaterializedBytes: peakObservedMaterializedBytes,
};
}
function fakeS3Path(s3Root: string, s3Uri: string): string {
const prefix = `s3://${FAKE_BUCKET}/`;
if (!s3Uri.startsWith(prefix)) {
throw new Error(`lambda-local: unexpected fake S3 URI ${s3Uri}`);
}
return join(s3Root, s3Uri.slice(prefix.length));
}
function requireV1PlanResult(
result: PlanLambdaResult,
): Extract<PlanLambdaResult, { PlanS3Uri: string }> {
if (!("PlanS3Uri" in result)) {
throw new Error("lambda-local: v1 plan returned v2 locators");
}
return result;
}
function requireV2PlanResult(
result: PlanLambdaResult,
): Extract<PlanLambdaResult, { PlanProtocol: "v2" }> {
if (!("PlanProtocol" in result) || result.PlanProtocol !== "v2") {
throw new Error("lambda-local: v2 plan did not return explicit v2 locators");
}
return result;
}
// The recursive walk is the measurement itself; extracting its two filesystem
// branches would make this small test-harness utility harder to audit.
// fallow-ignore-next-line complexity
function measureDirectoryBytes(path: string): number {
if (!existsSync(path)) return 0;
let bytes = 0;
for (const entry of readdirSync(path, { withFileTypes: true })) {
const child = join(path, entry.name);
if (entry.isDirectory()) {
bytes += measureDirectoryBytes(child);
} else if (entry.isFile()) {
bytes += statSync(child).size;
}
}
return bytes;
}
/**
@@ -162,8 +295,19 @@ export async function runLambdaLocalRender(input: RunLambdaLocalInput): Promise<
* without going through a real S3 endpoint.
*/
class FilesystemBackedFakeS3 {
private downloadedBytes = 0;
private uploadedBytes = 0;
private readonly metadata = new Map<string, Record<string, string>>();
constructor(private readonly root: string) {}
get transferBytes(): { downloaded: number; uploaded: number } {
return {
downloaded: this.downloadedBytes,
uploaded: this.uploadedBytes,
};
}
async send(command: unknown): Promise<unknown> {
const cmdName = (command as { constructor: { name: string } }).constructor.name;
const input = (command as { input: { Bucket: string; Key: string; Body?: unknown } }).input;
@@ -180,6 +324,7 @@ class FilesystemBackedFakeS3 {
throw err;
}
const bytes = readFileSync(fsPath);
this.downloadedBytes += bytes.length;
return { Body: Readable.from([bytes]) };
}
if (cmdName === "PutObjectCommand") {
@@ -192,7 +337,11 @@ class FilesystemBackedFakeS3 {
} else {
throw new Error(`FakeS3: PutObject body shape not supported (${typeof body})`);
}
return { ETag: `"fake-${statSync(fsPath).size}"` };
const size = statSync(fsPath).size;
this.uploadedBytes += size;
const metadata = (command as { input: { Metadata?: Record<string, string> } }).input.Metadata;
if (metadata) this.metadata.set(input.Key, metadata);
return { ETag: `"fake-${size}"` };
}
if (cmdName === "HeadObjectCommand") {
if (!existsSync(fsPath)) {
@@ -204,7 +353,11 @@ class FilesystemBackedFakeS3 {
err.$metadata = { httpStatusCode: 404 };
throw err;
}
return { ContentLength: statSync(fsPath).size, LastModified: new Date() };
return {
ContentLength: statSync(fsPath).size,
LastModified: new Date(),
Metadata: this.metadata.get(input.Key),
};
}
throw new Error(`FakeS3: unexpected command ${cmdName}`);
}