@hyperframes/aws-lambda 0.8.166 → 0.8.168

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/events.d.ts CHANGED
@@ -141,7 +141,7 @@ interface PlanLambdaResultBase {
141
141
  PlanHash: string;
142
142
  ChunkCount: number;
143
143
  TotalFrames: number;
144
- Fps: 24 | 30 | 60;
144
+ Fps: SerializableDistributedRenderConfig["fps"];
145
145
  Width: number;
146
146
  Height: number;
147
147
  Format: DistributedFormat;
package/dist/handler.d.ts CHANGED
@@ -32,6 +32,11 @@ export interface HandlerDeps {
32
32
  tmpRoot?: string;
33
33
  /** Skip Chrome resolution (used by handler dispatch tests that mock renderChunk). */
34
34
  skipChromeResolution?: boolean;
35
+ /**
36
+ * The Lambda runtime's `Context` arrives as this second argument and supplies it. Plan
37
+ * and chunk share one function, so its remaining time at plan start is the chunk limit.
38
+ */
39
+ getRemainingTimeInMillis?: () => number;
35
40
  }
36
41
  /**
37
42
  * Lambda entry. Step Functions sometimes wraps the event in
package/dist/handler.js CHANGED
@@ -166,14 +166,6 @@ async function uploadContentAddressedFileToS3(client, localPath, uri, expectedSh
166
166
  if (!existsSync2(localPath)) {
167
167
  throw new Error(`[s3Transport] upload source missing: ${localPath}`);
168
168
  }
169
- const actualSha256 = await sha256File(localPath);
170
- if (actualSha256 !== expectedSha256) {
171
- const error = new Error(
172
- `[s3Transport] PLAN_ARTIFACT_DIGEST_MISMATCH: local artifact ${localPath} expected ${expectedSha256}, got ${actualSha256}`
173
- );
174
- error.name = "PLAN_ARTIFACT_DIGEST_MISMATCH";
175
- throw error;
176
- }
177
169
  const { bucket, key } = parseS3Uri(uri);
178
170
  const size = statSync(localPath).size;
179
171
  const existing = await inspectContentAddressedObject(client, bucket, key, size, expectedSha256);
@@ -330,6 +322,7 @@ var S3PlanV2ArtifactPublisher = class {
330
322
  async putBlob(blob) {
331
323
  this.#assertOpen("publish a blob");
332
324
  const digest = assertSha2562(blob.sha256, "S3 published blob sha256");
325
+ if (this.#publishedDigests.has(digest)) return;
333
326
  const sourceSize = statSync2(blob.sourcePath).size;
334
327
  if (sourceSize !== blob.sizeBytes) {
335
328
  throw new PlanV2IntegrityError(
@@ -421,7 +414,7 @@ async function handler(event, deps) {
421
414
  function normalizeTerminalErrorName(error) {
422
415
  if (!error || typeof error !== "object") return;
423
416
  const candidate = error;
424
- if (candidate.code === "PLAN_PROTOCOL_UNSUPPORTED" || candidate.code === "PLAN_TOO_LARGE" || candidate.code === "PLAN_V2_INTEGRITY_UNRECOVERABLE" || candidate.code === "FONT_FETCH_FAILED" || candidate.code === "FONT_FETCH_UNAVAILABLE" || candidate.code === "NOT_MEDIA_PAYLOAD" || candidate.code === "VIDEO_SOURCE_UNRENDERABLE" || candidate.code === "VIDEO_EXTRACTION_FAILED" || candidate.code === "INVALID_VIDEO_METADATA") {
417
+ if (candidate.code === "PLAN_PROTOCOL_UNSUPPORTED" || candidate.code === "PLAN_TOO_LARGE" || candidate.code === "PLAN_V2_INTEGRITY_UNRECOVERABLE" || candidate.code === "FONT_FETCH_FAILED" || candidate.code === "FONT_FETCH_UNAVAILABLE" || candidate.code === "NOT_MEDIA_PAYLOAD" || candidate.code === "VIDEO_SOURCE_UNRENDERABLE" || candidate.code === "VIDEO_EXTRACTION_FAILED" || candidate.code === "INVALID_VIDEO_METADATA" || candidate.code === "FORMAT_NOT_SUPPORTED_IN_DISTRIBUTED" || candidate.code === "CHUNK_EXCEEDS_WORKER_TIMEOUT") {
425
418
  candidate.name = candidate.code;
426
419
  }
427
420
  }
@@ -522,11 +515,19 @@ function primeRuntimeEnv() {
522
515
  process.env.PATH = `${bin}:${process.env.PATH ?? ""}`;
523
516
  }
524
517
  }
518
+ function planConfig(event, deps) {
519
+ const remainingMs = deps?.getRemainingTimeInMillis?.();
520
+ if (event.Config.workerTimeoutSeconds !== void 0 || remainingMs === void 0) {
521
+ return { ...event.Config };
522
+ }
523
+ return { ...event.Config, workerTimeoutSeconds: Math.ceil(remainingMs / 1e3) };
524
+ }
525
525
  async function handlePlan(event, deps) {
526
526
  if (event.PlanProtocol !== "v1") {
527
527
  return handlePlanV2(event, deps);
528
528
  }
529
529
  const started = Date.now();
530
+ const config = planConfig(event, deps);
530
531
  const s3 = deps?.s3 ?? getS3Client();
531
532
  const primitive = deps?.primitives?.plan ?? plan;
532
533
  if (!deps?.skipChromeResolution && !process.env.PRODUCER_HEADLESS_SHELL_PATH) {
@@ -540,9 +541,6 @@ async function handlePlan(event, deps) {
540
541
  try {
541
542
  await downloadS3ObjectToFile(s3, event.ProjectS3Uri, projectArchive);
542
543
  await untarDirectory(projectArchive, projectDir);
543
- const config = {
544
- ...event.Config
545
- };
546
544
  const result = await primitive(projectDir, config, planDir);
547
545
  const planTar = join2(work, "plan.tar.gz");
548
546
  await tarDirectory(planDir, planTar);
@@ -576,6 +574,7 @@ async function handlePlan(event, deps) {
576
574
  }
577
575
  async function handlePlanV2(event, deps) {
578
576
  const started = Date.now();
577
+ const config = planConfig(event, deps);
579
578
  const s3 = deps?.s3 ?? getS3Client();
580
579
  const primitive = deps?.primitives?.planV2WithPublisher ?? planV2WithPublisher;
581
580
  if (!deps?.skipChromeResolution && !process.env.PRODUCER_HEADLESS_SHELL_PATH) {
@@ -585,14 +584,18 @@ async function handlePlanV2(event, deps) {
585
584
  const projectArchive = join2(work, "project.tar.gz");
586
585
  const projectDir = join2(work, "project");
587
586
  try {
587
+ let stageStarted = Date.now();
588
588
  await downloadS3ObjectToFile(s3, event.ProjectS3Uri, projectArchive);
589
+ logEvent({ event: "plan_stage", stage: "download", ms: Date.now() - stageStarted });
590
+ stageStarted = Date.now();
589
591
  await untarDirectory(projectArchive, projectDir);
592
+ logEvent({ event: "plan_stage", stage: "untar", ms: Date.now() - stageStarted });
590
593
  const publisher = new S3PlanV2ArtifactPublisher({
591
594
  s3,
592
595
  planOutputS3Prefix: event.PlanOutputS3Prefix,
593
596
  temporaryRoot: work
594
597
  });
595
- const manifest = await primitive(projectDir, { ...event.Config }, publisher, {
598
+ const manifest = await primitive(projectDir, config, publisher, {
596
599
  stagingParentDir: work
597
600
  });
598
601
  return {
@@ -761,11 +764,14 @@ async function handleAssembleV2(event, deps) {
761
764
  try {
762
765
  const planDir = await downloadAndMaterializePlanV2(s3, event, { role: "assembler" }, work);
763
766
  const audioPath = resolvePlanAudioPath(planDir);
767
+ const chunksStarted = Date.now();
764
768
  const chunkPaths = await downloadChunkObjects(s3, event.ChunkS3Uris, work, event.Format);
769
+ const chunksMs = Date.now() - chunksStarted;
765
770
  const finalOutput = event.Format === "png-sequence" ? join2(work, "output-frames") : join2(work, `output${formatExtension(event.Format)}`);
766
771
  const result = await primitive(planDir, chunkPaths, audioPath, finalOutput, {
767
772
  cfr: event.Cfr === true
768
773
  });
774
+ const uploadStarted = Date.now();
769
775
  if (event.Format === "png-sequence") {
770
776
  const tarball = `${finalOutput}.tar.gz`;
771
777
  await tarDirectory(finalOutput, tarball);
@@ -773,6 +779,13 @@ async function handleAssembleV2(event, deps) {
773
779
  } else {
774
780
  await uploadFileToS3(s3, finalOutput, event.OutputS3Uri);
775
781
  }
782
+ logEvent({
783
+ event: "assemble_stages",
784
+ planMs: chunksStarted - started,
785
+ chunksMs,
786
+ assembleMs: result.durationMs,
787
+ uploadMs: Date.now() - uploadStarted
788
+ });
776
789
  return {
777
790
  Action: "assemble",
778
791
  OutputS3Uri: event.OutputS3Uri,
package/dist/index.js CHANGED
@@ -174,14 +174,6 @@ async function uploadContentAddressedFileToS3(client, localPath, uri, expectedSh
174
174
  if (!existsSync2(localPath)) {
175
175
  throw new Error(`[s3Transport] upload source missing: ${localPath}`);
176
176
  }
177
- const actualSha256 = await sha256File(localPath);
178
- if (actualSha256 !== expectedSha256) {
179
- const error = new Error(
180
- `[s3Transport] PLAN_ARTIFACT_DIGEST_MISMATCH: local artifact ${localPath} expected ${expectedSha256}, got ${actualSha256}`
181
- );
182
- error.name = "PLAN_ARTIFACT_DIGEST_MISMATCH";
183
- throw error;
184
- }
185
177
  const { bucket, key } = parseS3Uri(uri);
186
178
  const size = statSync(localPath).size;
187
179
  const existing = await inspectContentAddressedObject(client, bucket, key, size, expectedSha256);
@@ -338,6 +330,7 @@ var S3PlanV2ArtifactPublisher = class {
338
330
  async putBlob(blob) {
339
331
  this.#assertOpen("publish a blob");
340
332
  const digest = assertSha2562(blob.sha256, "S3 published blob sha256");
333
+ if (this.#publishedDigests.has(digest)) return;
341
334
  const sourceSize = statSync2(blob.sourcePath).size;
342
335
  if (sourceSize !== blob.sizeBytes) {
343
336
  throw new PlanV2IntegrityError(
@@ -429,7 +422,7 @@ async function handler(event, deps) {
429
422
  function normalizeTerminalErrorName(error) {
430
423
  if (!error || typeof error !== "object") return;
431
424
  const candidate = error;
432
- if (candidate.code === "PLAN_PROTOCOL_UNSUPPORTED" || candidate.code === "PLAN_TOO_LARGE" || candidate.code === "PLAN_V2_INTEGRITY_UNRECOVERABLE" || candidate.code === "FONT_FETCH_FAILED" || candidate.code === "FONT_FETCH_UNAVAILABLE" || candidate.code === "NOT_MEDIA_PAYLOAD" || candidate.code === "VIDEO_SOURCE_UNRENDERABLE" || candidate.code === "VIDEO_EXTRACTION_FAILED" || candidate.code === "INVALID_VIDEO_METADATA") {
425
+ if (candidate.code === "PLAN_PROTOCOL_UNSUPPORTED" || candidate.code === "PLAN_TOO_LARGE" || candidate.code === "PLAN_V2_INTEGRITY_UNRECOVERABLE" || candidate.code === "FONT_FETCH_FAILED" || candidate.code === "FONT_FETCH_UNAVAILABLE" || candidate.code === "NOT_MEDIA_PAYLOAD" || candidate.code === "VIDEO_SOURCE_UNRENDERABLE" || candidate.code === "VIDEO_EXTRACTION_FAILED" || candidate.code === "INVALID_VIDEO_METADATA" || candidate.code === "FORMAT_NOT_SUPPORTED_IN_DISTRIBUTED" || candidate.code === "CHUNK_EXCEEDS_WORKER_TIMEOUT") {
433
426
  candidate.name = candidate.code;
434
427
  }
435
428
  }
@@ -530,11 +523,19 @@ function primeRuntimeEnv() {
530
523
  process.env.PATH = `${bin}:${process.env.PATH ?? ""}`;
531
524
  }
532
525
  }
526
+ function planConfig(event, deps) {
527
+ const remainingMs = deps?.getRemainingTimeInMillis?.();
528
+ if (event.Config.workerTimeoutSeconds !== void 0 || remainingMs === void 0) {
529
+ return { ...event.Config };
530
+ }
531
+ return { ...event.Config, workerTimeoutSeconds: Math.ceil(remainingMs / 1e3) };
532
+ }
533
533
  async function handlePlan(event, deps) {
534
534
  if (event.PlanProtocol !== "v1") {
535
535
  return handlePlanV2(event, deps);
536
536
  }
537
537
  const started = Date.now();
538
+ const config = planConfig(event, deps);
538
539
  const s3 = deps?.s3 ?? getS3Client();
539
540
  const primitive = deps?.primitives?.plan ?? plan;
540
541
  if (!deps?.skipChromeResolution && !process.env.PRODUCER_HEADLESS_SHELL_PATH) {
@@ -548,9 +549,6 @@ async function handlePlan(event, deps) {
548
549
  try {
549
550
  await downloadS3ObjectToFile(s3, event.ProjectS3Uri, projectArchive);
550
551
  await untarDirectory(projectArchive, projectDir);
551
- const config = {
552
- ...event.Config
553
- };
554
552
  const result = await primitive(projectDir, config, planDir);
555
553
  const planTar = join2(work, "plan.tar.gz");
556
554
  await tarDirectory(planDir, planTar);
@@ -584,6 +582,7 @@ async function handlePlan(event, deps) {
584
582
  }
585
583
  async function handlePlanV2(event, deps) {
586
584
  const started = Date.now();
585
+ const config = planConfig(event, deps);
587
586
  const s3 = deps?.s3 ?? getS3Client();
588
587
  const primitive = deps?.primitives?.planV2WithPublisher ?? planV2WithPublisher;
589
588
  if (!deps?.skipChromeResolution && !process.env.PRODUCER_HEADLESS_SHELL_PATH) {
@@ -593,14 +592,18 @@ async function handlePlanV2(event, deps) {
593
592
  const projectArchive = join2(work, "project.tar.gz");
594
593
  const projectDir = join2(work, "project");
595
594
  try {
595
+ let stageStarted = Date.now();
596
596
  await downloadS3ObjectToFile(s3, event.ProjectS3Uri, projectArchive);
597
+ logEvent({ event: "plan_stage", stage: "download", ms: Date.now() - stageStarted });
598
+ stageStarted = Date.now();
597
599
  await untarDirectory(projectArchive, projectDir);
600
+ logEvent({ event: "plan_stage", stage: "untar", ms: Date.now() - stageStarted });
598
601
  const publisher = new S3PlanV2ArtifactPublisher({
599
602
  s3,
600
603
  planOutputS3Prefix: event.PlanOutputS3Prefix,
601
604
  temporaryRoot: work
602
605
  });
603
- const manifest = await primitive(projectDir, { ...event.Config }, publisher, {
606
+ const manifest = await primitive(projectDir, config, publisher, {
604
607
  stagingParentDir: work
605
608
  });
606
609
  return {
@@ -769,11 +772,14 @@ async function handleAssembleV2(event, deps) {
769
772
  try {
770
773
  const planDir = await downloadAndMaterializePlanV2(s3, event, { role: "assembler" }, work);
771
774
  const audioPath = resolvePlanAudioPath(planDir);
775
+ const chunksStarted = Date.now();
772
776
  const chunkPaths = await downloadChunkObjects(s3, event.ChunkS3Uris, work, event.Format);
777
+ const chunksMs = Date.now() - chunksStarted;
773
778
  const finalOutput = event.Format === "png-sequence" ? join2(work, "output-frames") : join2(work, `output${formatExtension(event.Format)}`);
774
779
  const result = await primitive(planDir, chunkPaths, audioPath, finalOutput, {
775
780
  cfr: event.Cfr === true
776
781
  });
782
+ const uploadStarted = Date.now();
777
783
  if (event.Format === "png-sequence") {
778
784
  const tarball = `${finalOutput}.tar.gz`;
779
785
  await tarDirectory(finalOutput, tarball);
@@ -781,6 +787,13 @@ async function handleAssembleV2(event, deps) {
781
787
  } else {
782
788
  await uploadFileToS3(s3, finalOutput, event.OutputS3Uri);
783
789
  }
790
+ logEvent({
791
+ event: "assemble_stages",
792
+ planMs: chunksStarted - started,
793
+ chunksMs,
794
+ assembleMs: result.durationMs,
795
+ uploadMs: Date.now() - uploadStarted
796
+ });
784
797
  return {
785
798
  Action: "assemble",
786
799
  OutputS3Uri: event.OutputS3Uri,
@@ -39,11 +39,9 @@ export declare function downloadS3ObjectToFileVerified(client: S3Client, uri: st
39
39
  */
40
40
  export declare function uploadFileToS3(client: S3Client, localPath: string, uri: string, contentType?: string): Promise<void>;
41
41
  /**
42
- * Upload one content-addressed plan-v2 artifact exactly once.
43
- *
44
- * Existing objects are reused only when their immutable digest metadata and
45
- * byte length agree. A conflicting object is never overwritten: doing so
46
- * could change a plan already being consumed by another chunk invocation.
42
+ * Upload one content-addressed plan-v2 artifact exactly once; S3 verifies the bytes against
43
+ * `ChecksumSHA256`. An existing object is reused only when its digest metadata and length agree,
44
+ * and a conflicting one is never overwritten, since a chunk may already be reading that plan.
47
45
  */
48
46
  export declare function uploadContentAddressedFileToS3(client: S3Client, localPath: string, uri: string, expectedSha256: string, contentType?: string): Promise<"uploaded" | "reused">;
49
47
  export declare function sha256File(path: string): Promise<string>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperframes/aws-lambda",
3
- "version": "0.8.166",
3
+ "version": "0.8.168",
4
4
  "description": "AWS Lambda adapter for HyperFrames distributed rendering — handler, client-side SDK, and CDK construct.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -45,7 +45,7 @@
45
45
  "ffprobe-static": "^3.1.0",
46
46
  "puppeteer-core": "^25.8.0",
47
47
  "tar": "^7.4.3",
48
- "@hyperframes/producer": "^0.8.166"
48
+ "@hyperframes/producer": "^0.8.168"
49
49
  },
50
50
  "devDependencies": {
51
51
  "@types/aws-lambda": "^8.10.146",