(event: PlanEvent, deps?: HandlerDeps)
| 226 | // ── Plan ──────────────────────────────────────────────────────────────────── |
| 227 | |
| 228 | async function handlePlan(event: PlanEvent, deps?: HandlerDeps): Promise<PlanLambdaResult> { |
| 229 | const started = Date.now(); |
| 230 | const s3 = deps?.s3 ?? getS3Client(); |
| 231 | const primitive = deps?.primitives?.plan ?? plan; |
| 232 | |
| 233 | // The producer's probe stage launches Chromium whenever the composition |
| 234 | // needs a runtime duration probe or has unresolved sub-compositions, so |
| 235 | // plan has to resolve Chrome the same way renderChunk does. Without this |
| 236 | // the probe throws "An `executablePath` or `channel` must be specified |
| 237 | // for `puppeteer-core`" the moment runProbeStage calls puppeteer.launch. |
| 238 | if (!deps?.skipChromeResolution && !process.env.PRODUCER_HEADLESS_SHELL_PATH) { |
| 239 | const chromePath = await resolveChromeExecutablePath(); |
| 240 | process.env.PRODUCER_HEADLESS_SHELL_PATH = chromePath; |
| 241 | } |
| 242 | |
| 243 | const work = mkdtempSync(join(deps?.tmpRoot ?? tmpdir(), "hf-lambda-plan-")); |
| 244 | // We use `.tar.gz` (not `.zip`) as the project archive's on-the-wire |
| 245 | // format because Lambda's Amazon Linux base image ships GNU `tar` but |
| 246 | // not `unzip` in `/usr/bin`. The smoke script + future CLI both |
| 247 | // produce tar.gz uploads. |
| 248 | const projectArchive = join(work, "project.tar.gz"); |
| 249 | const projectDir = join(work, "project"); |
| 250 | const planDir = join(work, "plan"); |
| 251 | |
| 252 | try { |
| 253 | await downloadS3ObjectToFile(s3, event.ProjectS3Uri, projectArchive); |
| 254 | await untarDirectory(projectArchive, projectDir); |
| 255 | |
| 256 | const config: DistributedRenderConfig = { |
| 257 | ...event.Config, |
| 258 | }; |
| 259 | const result: PlanResult = await primitive(projectDir, config, planDir); |
| 260 | |
| 261 | // Upload the planDir as a single tarball. Step Functions cannot pass |
| 262 | // a directory-shaped artifact between states; we serialize and rely on |
| 263 | // the consumer (renderChunk / assemble) to untar. Audio is co-located |
| 264 | // alongside the plan so RenderChunk doesn't have to pull the whole |
| 265 | // plan tarball when audio isn't relevant to the chunk. |
| 266 | const planTar = join(work, "plan.tar.gz"); |
| 267 | await tarDirectory(planDir, planTar); |
| 268 | const planTarUri = `${trimTrailingSlash(event.PlanOutputS3Prefix)}/plan.tar.gz`; |
| 269 | const audioPath = join(planDir, "audio.aac"); |
| 270 | const hasAudio = existsSync(audioPath) && statSync(audioPath).size > 0; |
| 271 | const audioUri = hasAudio ? `${trimTrailingSlash(event.PlanOutputS3Prefix)}/audio.aac` : null; |
| 272 | // Plan and audio are independent S3 PUTs; run them in parallel so |
| 273 | // the response returns as soon as the slower of the two completes. |
| 274 | await Promise.all([ |
| 275 | uploadFileToS3(s3, planTar, planTarUri, "application/gzip"), |
| 276 | hasAudio && audioUri ? uploadFileToS3(s3, audioPath, audioUri, "audio/aac") : null, |
| 277 | ]); |
| 278 | |
| 279 | return { |
| 280 | Action: "plan", |
| 281 | PlanS3Uri: planTarUri, |
| 282 | PlanHash: result.planHash, |
| 283 | ChunkCount: result.chunkCount, |
| 284 | TotalFrames: result.totalFrames, |
| 285 | Fps: result.fps, |
no test coverage detected