@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 +1 -1
- package/dist/handler.d.ts +5 -0
- package/dist/handler.js +26 -13
- package/dist/index.js +26 -13
- package/dist/s3Transport.d.ts +3 -5
- package/package.json +2 -2
package/dist/events.d.ts
CHANGED
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,
|
|
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,
|
|
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,
|
package/dist/s3Transport.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
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.
|
|
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.
|
|
48
|
+
"@hyperframes/producer": "^0.8.168"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"@types/aws-lambda": "^8.10.146",
|