mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-05 10:14:30 +00:00
Add client-side validation for the new config.variables field (introduced in PR 9.1) and a 256 KiB cap on the full Step Functions Standard execution input. Both checks throw a typed InvalidConfigError BEFORE the SDK calls StartExecution — catching the obvious mistakes locally instead of as a States.DataLimitExceeded 50 ms into the execution. validateVariablesPayload walks the variables tree and rejects: - functions, Symbols, BigInts, non-finite numbers - undefined leaves (silently dropped by JSON.stringify — would surprise the caller when their value doesn't show up in the render) - non-plain objects (Date, Map, class instances) — Date's toJSON does round-trip as a string, but the composition gets a string, not a Date, so explicit reject is clearer validateStepFunctionsInputSize measures the actual UTF-8 byte length of JSON.stringify(input) against the 256 KiB cap. We use Standard workflows (per the plan §6.2 / §15.2) for execution-history visibility, so the cap is 256 KiB (Express would be 32 KiB). The error message names the actual byte count, the cap, and points at the templates-on-lambda#working-with-large-variables section so users know to URL-reference media assets instead of inlining them. Both helpers are exported from @hyperframes/aws-lambda/sdk so adapters that build custom Step Functions inputs (batch verbs, future Temporal ports) can reuse the same gates. Phase 9 PR 9.2 of the distributed rendering plan.
148 lines
5.4 KiB
TypeScript
148 lines
5.4 KiB
TypeScript
/**
|
|
* `renderToLambda` — start a distributed render against an already-deployed
|
|
* SAM/CDK stack and return a handle the caller can poll with
|
|
* {@link getRenderProgress}.
|
|
*
|
|
* The function does *not* wait for the render to finish. Step Functions
|
|
* standard workflows can run for hours; blocking the caller's process on
|
|
* the SFN execution is the wrong default. The returned `RenderHandle`
|
|
* carries everything the progress / cost / download paths need.
|
|
*
|
|
* Wire order:
|
|
* 1. Validate config (typed throw before any AWS call).
|
|
* 2. `deploySite` if no `siteHandle` was provided.
|
|
* 3. `StartExecution` against the state machine with the same input
|
|
* shape `examples/aws-lambda/scripts/smoke.sh` builds.
|
|
* 4. Return handle. The S3 `outputKey` is deterministic from the
|
|
* execution name so the caller can predict the final object URL.
|
|
*/
|
|
|
|
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 { formatExtension } from "../formatExtension.js";
|
|
import { formatS3Uri } from "../s3Transport.js";
|
|
import { deploySite, type SiteHandle } from "./deploySite.js";
|
|
import {
|
|
validateDistributedRenderConfig,
|
|
validateStepFunctionsInputSize,
|
|
} from "./validateConfig.js";
|
|
|
|
/** Options for {@link renderToLambda}. */
|
|
export interface RenderToLambdaOptions {
|
|
/** Local project directory. Required when `siteHandle` is not supplied. */
|
|
projectDir?: string;
|
|
/** Re-use an existing `deploySite` upload (skips tar+S3 PUT). */
|
|
siteHandle?: SiteHandle;
|
|
/** Validated `SerializableDistributedRenderConfig` (no logger / abortSignal). */
|
|
config: SerializableDistributedRenderConfig;
|
|
/** S3 bucket from the SAM stack output (`RenderBucketName`). */
|
|
bucketName: string;
|
|
/** State machine ARN from the SAM stack output (`RenderStateMachineArn`). */
|
|
stateMachineArn: string;
|
|
/** AWS region; defaults to the SDK default chain. */
|
|
region?: string;
|
|
/**
|
|
* Final output S3 key. Defaults to `renders/<executionName>/output.<ext>`
|
|
* where `<ext>` is derived from `config.format`.
|
|
*/
|
|
outputKey?: string;
|
|
/**
|
|
* Step Functions execution name. Defaults to `hf-render-<uuid>`.
|
|
* Used as `renderId` everywhere downstream (history queries, cost
|
|
* accounting, predictable S3 key prefix).
|
|
*/
|
|
executionName?: string;
|
|
/** Test injection seam — production callers leave unset. */
|
|
sfn?: SFNClient;
|
|
/** Test injection seam — propagated to `deploySite` when applicable. */
|
|
s3?: S3Client;
|
|
}
|
|
|
|
/** Stable identifier + every URL/ARN the caller needs to follow the render. */
|
|
export interface RenderHandle {
|
|
/** Same as the Step Functions execution name. */
|
|
renderId: string;
|
|
/** Full execution ARN; pass to {@link getRenderProgress}. */
|
|
executionArn: string;
|
|
bucketName: string;
|
|
stateMachineArn: string;
|
|
outputS3Uri: string;
|
|
projectS3Uri: string;
|
|
startedAt: string;
|
|
}
|
|
|
|
// fallow-ignore-next-line complexity
|
|
export async function renderToLambda(opts: RenderToLambdaOptions): Promise<RenderHandle> {
|
|
validateDistributedRenderConfig(opts.config);
|
|
|
|
if (!opts.bucketName) {
|
|
throw new Error("[renderToLambda] bucketName is required");
|
|
}
|
|
if (!opts.stateMachineArn) {
|
|
throw new Error("[renderToLambda] stateMachineArn is required");
|
|
}
|
|
if (!opts.siteHandle && !opts.projectDir) {
|
|
throw new Error("[renderToLambda] either siteHandle or projectDir must be supplied");
|
|
}
|
|
|
|
const executionName = opts.executionName ?? `hf-render-${randomUUID()}`;
|
|
const ext = formatExtension(opts.config.format);
|
|
const outputKey = opts.outputKey ?? `renders/${executionName}/output${ext}`;
|
|
const planOutputS3Prefix = formatS3Uri({
|
|
bucket: opts.bucketName,
|
|
key: `renders/${executionName}/`,
|
|
});
|
|
const outputS3Uri = formatS3Uri({ bucket: opts.bucketName, key: outputKey });
|
|
|
|
const site =
|
|
opts.siteHandle ??
|
|
(await deploySite({
|
|
projectDir: opts.projectDir as string,
|
|
bucketName: opts.bucketName,
|
|
region: opts.region,
|
|
s3: opts.s3,
|
|
}));
|
|
|
|
const input = {
|
|
ProjectS3Uri: site.projectS3Uri,
|
|
PlanOutputS3Prefix: planOutputS3Prefix,
|
|
OutputS3Uri: outputS3Uri,
|
|
Config: opts.config,
|
|
};
|
|
|
|
// Reject oversize input client-side. Step Functions Standard caps the
|
|
// execution input at 256 KiB; without this check, the input bloat
|
|
// (typically from `config.variables` containing inlined media) surfaces
|
|
// as `States.DataLimitExceeded` 50 ms into the execution, far from the
|
|
// caller's stack frame. Measured AFTER `deploySite` so the synthesised
|
|
// `ProjectS3Uri` is counted (a few hundred bytes either way, but the
|
|
// check should be against the actual wire payload).
|
|
validateStepFunctionsInputSize(input);
|
|
|
|
const sfn = opts.sfn ?? new SFNClient({ region: opts.region });
|
|
const startedAt = new Date().toISOString();
|
|
const response = await sfn.send(
|
|
new StartExecutionCommand({
|
|
stateMachineArn: opts.stateMachineArn,
|
|
name: executionName,
|
|
input: JSON.stringify(input),
|
|
}),
|
|
);
|
|
|
|
if (!response.executionArn) {
|
|
throw new Error("[renderToLambda] StartExecution returned no executionArn");
|
|
}
|
|
|
|
return {
|
|
renderId: executionName,
|
|
executionArn: response.executionArn,
|
|
bucketName: opts.bucketName,
|
|
stateMachineArn: opts.stateMachineArn,
|
|
outputS3Uri,
|
|
projectS3Uri: site.projectS3Uri,
|
|
startedAt,
|
|
};
|
|
}
|