@hyperframes/gcp-cloud-run 0.7.71 → 0.7.72
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/Dockerfile +1 -0
- package/dist/events.d.ts +71 -11
- package/dist/events.d.ts.map +1 -1
- package/dist/gcsTransport.d.ts +12 -0
- package/dist/gcsTransport.d.ts.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +338 -6
- package/dist/index.js.map +2 -2
- package/dist/sdk/index.js +10 -2
- package/dist/sdk/index.js.map +2 -2
- package/dist/sdk/renderToCloudRun.d.ts +6 -1
- package/dist/sdk/renderToCloudRun.d.ts.map +1 -1
- package/dist/server.d.ts +2 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +333 -5
- package/dist/server.js.map +2 -2
- package/package.json +4 -3
- package/terraform/main.tf +5 -0
- package/terraform/outputs.tf +10 -0
- package/terraform/smoke-safety.test.ts +71 -0
- package/terraform/variables.tf +9 -0
- package/terraform/workflow.test.ts +148 -0
- package/terraform/workflow.yaml +149 -10
package/dist/server.js
CHANGED
|
@@ -8,7 +8,11 @@ import { Storage } from "@google-cloud/storage";
|
|
|
8
8
|
import { Hono } from "hono";
|
|
9
9
|
import {
|
|
10
10
|
assemble,
|
|
11
|
+
listPlanV2ArtifactsForTarget,
|
|
12
|
+
materializePlanV2Target,
|
|
11
13
|
plan,
|
|
14
|
+
planV2,
|
|
15
|
+
readPlanV2Manifest,
|
|
12
16
|
renderChunk
|
|
13
17
|
} from "@hyperframes/producer/distributed";
|
|
14
18
|
|
|
@@ -74,7 +78,15 @@ function formatExtension(format) {
|
|
|
74
78
|
}
|
|
75
79
|
|
|
76
80
|
// src/gcsTransport.ts
|
|
77
|
-
import {
|
|
81
|
+
import {
|
|
82
|
+
createReadStream,
|
|
83
|
+
createWriteStream,
|
|
84
|
+
existsSync as existsSync2,
|
|
85
|
+
mkdirSync,
|
|
86
|
+
rmSync,
|
|
87
|
+
statSync
|
|
88
|
+
} from "node:fs";
|
|
89
|
+
import { createHash } from "node:crypto";
|
|
78
90
|
import { dirname } from "node:path";
|
|
79
91
|
import { pipeline } from "node:stream/promises";
|
|
80
92
|
import * as tar from "tar";
|
|
@@ -100,6 +112,19 @@ async function downloadGcsObjectToFile(storage, uri, destPath) {
|
|
|
100
112
|
const file = storage.bucket(bucket).file(key);
|
|
101
113
|
await pipeline(file.createReadStream(), createWriteStream(destPath));
|
|
102
114
|
}
|
|
115
|
+
async function downloadGcsObjectToFileVerified(storage, uri, destPath, expectedSha256) {
|
|
116
|
+
assertSha256(expectedSha256);
|
|
117
|
+
await downloadGcsObjectToFile(storage, uri, destPath);
|
|
118
|
+
const actual = await sha256File(destPath);
|
|
119
|
+
if (actual !== expectedSha256) {
|
|
120
|
+
rmSync(destPath, { force: true });
|
|
121
|
+
const error = new Error(
|
|
122
|
+
`[gcsTransport] PLAN_ARTIFACT_DIGEST_MISMATCH: ${uri} expected ${expectedSha256}, got ${actual}`
|
|
123
|
+
);
|
|
124
|
+
error.name = "PLAN_ARTIFACT_DIGEST_MISMATCH";
|
|
125
|
+
throw error;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
103
128
|
async function uploadFileToGcs(storage, localPath, uri, contentType) {
|
|
104
129
|
if (!existsSync2(localPath)) {
|
|
105
130
|
throw new Error(`[gcsTransport] upload source missing: ${localPath}`);
|
|
@@ -114,6 +139,73 @@ async function uploadFileToGcs(storage, localPath, uri, contentType) {
|
|
|
114
139
|
contentType
|
|
115
140
|
});
|
|
116
141
|
}
|
|
142
|
+
async function uploadContentAddressedFileToGcs(storage, localPath, uri, expectedSha256, contentType) {
|
|
143
|
+
assertSha256(expectedSha256);
|
|
144
|
+
if (!existsSync2(localPath)) {
|
|
145
|
+
throw new Error(`[gcsTransport] upload source missing: ${localPath}`);
|
|
146
|
+
}
|
|
147
|
+
const actualSha256 = await sha256File(localPath);
|
|
148
|
+
if (actualSha256 !== expectedSha256) {
|
|
149
|
+
throwDigestMismatch(
|
|
150
|
+
`local artifact ${localPath} expected ${expectedSha256}, got ${actualSha256}`
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
const { bucket, key } = parseGcsUri(uri);
|
|
154
|
+
const bucketHandle = storage.bucket(bucket);
|
|
155
|
+
const file = bucketHandle.file(key);
|
|
156
|
+
const size = statSync(localPath).size;
|
|
157
|
+
if (await isReusableContentAddressedObject(file, uri, size, expectedSha256)) {
|
|
158
|
+
return "reused";
|
|
159
|
+
}
|
|
160
|
+
try {
|
|
161
|
+
await bucketHandle.upload(localPath, {
|
|
162
|
+
destination: key,
|
|
163
|
+
contentType,
|
|
164
|
+
metadata: { metadata: { sha256: expectedSha256 } },
|
|
165
|
+
preconditionOpts: { ifGenerationMatch: 0 }
|
|
166
|
+
});
|
|
167
|
+
return "uploaded";
|
|
168
|
+
} catch (error) {
|
|
169
|
+
if (isGcsPreconditionFailed(error) && await isReusableContentAddressedObject(file, uri, size, expectedSha256)) {
|
|
170
|
+
return "reused";
|
|
171
|
+
}
|
|
172
|
+
throw error;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
async function isReusableContentAddressedObject(file, uri, expectedSize, expectedSha256) {
|
|
176
|
+
const [exists] = await file.exists();
|
|
177
|
+
if (!exists) return false;
|
|
178
|
+
const [metadata] = await file.getMetadata();
|
|
179
|
+
if (Number(metadata.size) === expectedSize && metadata.metadata?.sha256 === expectedSha256) {
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
182
|
+
throwDigestMismatch(
|
|
183
|
+
`immutable object ${uri} already exists with different digest metadata or size`
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
async function sha256File(path) {
|
|
187
|
+
const hash = createHash("sha256");
|
|
188
|
+
for await (const chunk of createReadStream(path)) {
|
|
189
|
+
hash.update(chunk);
|
|
190
|
+
}
|
|
191
|
+
return hash.digest("hex");
|
|
192
|
+
}
|
|
193
|
+
function assertSha256(value) {
|
|
194
|
+
if (!/^[a-f0-9]{64}$/.test(value)) {
|
|
195
|
+
throw new Error(
|
|
196
|
+
`[gcsTransport] expected lowercase SHA-256 digest, got ${JSON.stringify(value)}`
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
function throwDigestMismatch(detail) {
|
|
201
|
+
const error = new Error(`[gcsTransport] PLAN_ARTIFACT_DIGEST_MISMATCH: ${detail}`);
|
|
202
|
+
error.name = "PLAN_ARTIFACT_DIGEST_MISMATCH";
|
|
203
|
+
throw error;
|
|
204
|
+
}
|
|
205
|
+
function isGcsPreconditionFailed(error) {
|
|
206
|
+
if (!error || typeof error !== "object") return false;
|
|
207
|
+
return error.code === 412;
|
|
208
|
+
}
|
|
117
209
|
async function tarDirectory(sourceDir, destTarball) {
|
|
118
210
|
if (!existsSync2(sourceDir) || !statSync(sourceDir).isDirectory()) {
|
|
119
211
|
throw new Error(`[gcsTransport] tar source must be an existing directory: ${sourceDir}`);
|
|
@@ -141,6 +233,7 @@ function getStorage() {
|
|
|
141
233
|
}
|
|
142
234
|
async function dispatch(event, deps) {
|
|
143
235
|
const unwrapped = unwrapEvent(event);
|
|
236
|
+
validatePlanProtocolShape(unwrapped);
|
|
144
237
|
validateEventGcsUris(unwrapped);
|
|
145
238
|
logEvent({ event: "handler_start", action: unwrapped.Action, input: summarizeEvent(unwrapped) });
|
|
146
239
|
try {
|
|
@@ -161,15 +254,52 @@ async function dispatch(event, deps) {
|
|
|
161
254
|
}
|
|
162
255
|
}
|
|
163
256
|
} catch (err) {
|
|
257
|
+
normalizeTerminalErrorName(err);
|
|
164
258
|
logEvent({
|
|
165
259
|
event: "handler_error",
|
|
166
260
|
action: unwrapped.Action,
|
|
261
|
+
input: summarizeEvent(unwrapped),
|
|
167
262
|
message: err instanceof Error ? err.message : String(err),
|
|
168
263
|
name: err instanceof Error ? err.name : void 0
|
|
169
264
|
});
|
|
170
265
|
throw err;
|
|
171
266
|
}
|
|
172
267
|
}
|
|
268
|
+
function validatePlanProtocolShape(event) {
|
|
269
|
+
const raw = event;
|
|
270
|
+
const protocol = raw.PlanProtocol;
|
|
271
|
+
if (protocol !== void 0 && protocol !== "v1" && protocol !== "v2") {
|
|
272
|
+
const error = new Error(
|
|
273
|
+
`[handler] unsupported PlanProtocol ${JSON.stringify(protocol)}; expected "v1", "v2", or absent`
|
|
274
|
+
);
|
|
275
|
+
error.name = "PLAN_PROTOCOL_UNSUPPORTED";
|
|
276
|
+
throw error;
|
|
277
|
+
}
|
|
278
|
+
if (event.Action === "plan") return;
|
|
279
|
+
const hasV1Locator = typeof raw.PlanGcsUri === "string";
|
|
280
|
+
const hasV2Manifest = typeof raw.PlanV2ManifestGcsUri === "string";
|
|
281
|
+
const hasV2Prefix = typeof raw.PlanV2ArtifactGcsPrefix === "string";
|
|
282
|
+
const valid = protocol === "v2" ? !hasV1Locator && hasV2Manifest && hasV2Prefix : hasV1Locator && !hasV2Manifest && !hasV2Prefix;
|
|
283
|
+
if (!valid) {
|
|
284
|
+
const error = new Error(
|
|
285
|
+
`[handler] ${protocol === "v2" ? "v2" : "v1"} ${event.Action} event has mixed or missing plan locators`
|
|
286
|
+
);
|
|
287
|
+
error.name = "PLAN_PROTOCOL_UNSUPPORTED";
|
|
288
|
+
throw error;
|
|
289
|
+
}
|
|
290
|
+
if (protocol === "v2" && event.Action === "assemble" && event.AudioGcsUri !== null) {
|
|
291
|
+
const error = new Error("[handler] v2 assemble audio must be materialized from the manifest");
|
|
292
|
+
error.name = "PLAN_PROTOCOL_UNSUPPORTED";
|
|
293
|
+
throw error;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
function normalizeTerminalErrorName(error) {
|
|
297
|
+
if (!error || typeof error !== "object") return;
|
|
298
|
+
const candidate = error;
|
|
299
|
+
if (candidate.code === "PLAN_PROTOCOL_UNSUPPORTED" || candidate.code === "PLAN_TOO_LARGE" || candidate.code === "PLAN_V2_INTEGRITY_UNRECOVERABLE") {
|
|
300
|
+
candidate.name = candidate.code;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
173
303
|
var MAX_ENVELOPE_DEPTH = 4;
|
|
174
304
|
function unwrapEvent(event) {
|
|
175
305
|
let cursor = event;
|
|
@@ -206,18 +336,21 @@ function summarizeEvent(event) {
|
|
|
206
336
|
return {
|
|
207
337
|
projectGcsUri: event.ProjectGcsUri,
|
|
208
338
|
planOutputGcsPrefix: event.PlanOutputGcsPrefix,
|
|
339
|
+
planProtocol: event.PlanProtocol ?? "v1",
|
|
209
340
|
format: event.Config.format,
|
|
210
341
|
fps: event.Config.fps
|
|
211
342
|
};
|
|
212
343
|
case "renderChunk":
|
|
213
344
|
return {
|
|
214
|
-
|
|
345
|
+
planProtocol: event.PlanProtocol ?? "v1",
|
|
346
|
+
...event.PlanProtocol === "v2" ? { planV2ManifestGcsUri: event.PlanV2ManifestGcsUri } : { planGcsUri: event.PlanGcsUri },
|
|
215
347
|
chunkIndex: event.ChunkIndex,
|
|
216
348
|
format: event.Format
|
|
217
349
|
};
|
|
218
350
|
case "assemble":
|
|
219
351
|
return {
|
|
220
|
-
|
|
352
|
+
planProtocol: event.PlanProtocol ?? "v1",
|
|
353
|
+
...event.PlanProtocol === "v2" ? { planV2ManifestGcsUri: event.PlanV2ManifestGcsUri } : { planGcsUri: event.PlanGcsUri },
|
|
221
354
|
chunkCount: event.ChunkGcsUris.length,
|
|
222
355
|
hasAudio: event.AudioGcsUri !== null,
|
|
223
356
|
outputGcsUri: event.OutputGcsUri,
|
|
@@ -231,6 +364,9 @@ function primeChrome(deps) {
|
|
|
231
364
|
process.env.PRODUCER_HEADLESS_SHELL_PATH = resolveChromeExecutablePath();
|
|
232
365
|
}
|
|
233
366
|
async function handlePlan(event, deps) {
|
|
367
|
+
if (event.PlanProtocol === "v2") {
|
|
368
|
+
return handlePlanV2(event, deps);
|
|
369
|
+
}
|
|
234
370
|
const started = Date.now();
|
|
235
371
|
const storage = deps?.storage ?? getStorage();
|
|
236
372
|
const primitive = deps?.primitives?.plan ?? plan;
|
|
@@ -272,7 +408,70 @@ async function handlePlan(event, deps) {
|
|
|
272
408
|
cleanupDir(work);
|
|
273
409
|
}
|
|
274
410
|
}
|
|
411
|
+
async function handlePlanV2(event, deps) {
|
|
412
|
+
const started = Date.now();
|
|
413
|
+
const storage = deps?.storage ?? getStorage();
|
|
414
|
+
const primitive = deps?.primitives?.planV2 ?? planV2;
|
|
415
|
+
primeChrome(deps);
|
|
416
|
+
const work = mkdtempSync(join(deps?.tmpRoot ?? tmpdir(), "hf-cr-plan-v2-"));
|
|
417
|
+
const projectArchive = join(work, "project.tar.gz");
|
|
418
|
+
const projectDir = join(work, "project");
|
|
419
|
+
const planV2Dir = join(work, "plan-v2");
|
|
420
|
+
try {
|
|
421
|
+
await downloadGcsObjectToFile(storage, event.ProjectGcsUri, projectArchive);
|
|
422
|
+
await untarDirectory(projectArchive, projectDir);
|
|
423
|
+
const result = await primitive(projectDir, { ...event.Config }, planV2Dir);
|
|
424
|
+
const manifest = readPlanV2Manifest(planV2Dir);
|
|
425
|
+
if (manifest.planHash !== result.planHash) {
|
|
426
|
+
throwPlanHashMismatch(result.planHash, manifest.planHash);
|
|
427
|
+
}
|
|
428
|
+
const outputPrefix = `${trimTrailingSlash(event.PlanOutputGcsPrefix)}/v2`;
|
|
429
|
+
const artifactPrefix = `${outputPrefix}/artifacts/sha256`;
|
|
430
|
+
const uniqueArtifacts = [
|
|
431
|
+
...new Map(manifest.artifacts.map((artifact) => [artifact.sha256, artifact])).values()
|
|
432
|
+
];
|
|
433
|
+
await mapConcurrent(uniqueArtifacts, 16, async (artifact) => {
|
|
434
|
+
await uploadContentAddressedFileToGcs(
|
|
435
|
+
storage,
|
|
436
|
+
planV2BlobPath(planV2Dir, artifact.sha256),
|
|
437
|
+
planV2BlobUri(artifactPrefix, artifact.sha256),
|
|
438
|
+
artifact.sha256
|
|
439
|
+
);
|
|
440
|
+
});
|
|
441
|
+
const manifestUri = `${outputPrefix}/manifest.json`;
|
|
442
|
+
await uploadContentAddressedFileToGcs(
|
|
443
|
+
storage,
|
|
444
|
+
result.manifestPath,
|
|
445
|
+
manifestUri,
|
|
446
|
+
await sha256File(result.manifestPath),
|
|
447
|
+
"application/json"
|
|
448
|
+
);
|
|
449
|
+
return {
|
|
450
|
+
Action: "plan",
|
|
451
|
+
PlanProtocol: "v2",
|
|
452
|
+
PlanV2ManifestGcsUri: manifestUri,
|
|
453
|
+
PlanV2ArtifactGcsPrefix: artifactPrefix,
|
|
454
|
+
PlanHash: result.planHash,
|
|
455
|
+
ChunkCount: result.chunkCount,
|
|
456
|
+
TotalFrames: result.totalFrames,
|
|
457
|
+
Fps: result.fps,
|
|
458
|
+
Width: result.width,
|
|
459
|
+
Height: result.height,
|
|
460
|
+
Format: result.format,
|
|
461
|
+
HasAudio: manifest.artifacts.some((artifact) => artifact.path === "audio.aac"),
|
|
462
|
+
AudioGcsUri: null,
|
|
463
|
+
FfmpegVersion: result.ffmpegVersion,
|
|
464
|
+
ProducerVersion: result.producerVersion,
|
|
465
|
+
DurationMs: Date.now() - started
|
|
466
|
+
};
|
|
467
|
+
} finally {
|
|
468
|
+
cleanupDir(work);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
275
471
|
async function handleRenderChunk(event, deps) {
|
|
472
|
+
if (event.PlanProtocol === "v2") {
|
|
473
|
+
return handleRenderChunkV2(event, deps);
|
|
474
|
+
}
|
|
276
475
|
const started = Date.now();
|
|
277
476
|
const storage = deps?.storage ?? getStorage();
|
|
278
477
|
const primitive = deps?.primitives?.renderChunk ?? renderChunk;
|
|
@@ -307,6 +506,42 @@ async function handleRenderChunk(event, deps) {
|
|
|
307
506
|
cleanupDir(work);
|
|
308
507
|
}
|
|
309
508
|
}
|
|
509
|
+
async function handleRenderChunkV2(event, deps) {
|
|
510
|
+
const started = Date.now();
|
|
511
|
+
const storage = deps?.storage ?? getStorage();
|
|
512
|
+
const primitive = deps?.primitives?.renderChunk ?? renderChunk;
|
|
513
|
+
primeChrome(deps);
|
|
514
|
+
const work = mkdtempSync(join(deps?.tmpRoot ?? tmpdir(), "hf-cr-chunk-v2-"));
|
|
515
|
+
try {
|
|
516
|
+
const planDir = await downloadAndMaterializePlanV2(
|
|
517
|
+
storage,
|
|
518
|
+
event,
|
|
519
|
+
{ role: "chunk", chunkIndex: event.ChunkIndex },
|
|
520
|
+
work
|
|
521
|
+
);
|
|
522
|
+
const chunkOutputBase = join(
|
|
523
|
+
work,
|
|
524
|
+
event.Format === "png-sequence" ? `chunk-${pad(event.ChunkIndex)}` : `chunk-${pad(event.ChunkIndex)}${formatExtension(event.Format)}`
|
|
525
|
+
);
|
|
526
|
+
const result = await primitive(planDir, event.ChunkIndex, chunkOutputBase);
|
|
527
|
+
const chunkUri = await uploadChunkOutput(
|
|
528
|
+
storage,
|
|
529
|
+
result,
|
|
530
|
+
event.ChunkOutputGcsPrefix,
|
|
531
|
+
event.ChunkIndex
|
|
532
|
+
);
|
|
533
|
+
return {
|
|
534
|
+
Action: "renderChunk",
|
|
535
|
+
ChunkGcsUri: chunkUri,
|
|
536
|
+
ChunkIndex: event.ChunkIndex,
|
|
537
|
+
Sha256: result.sha256,
|
|
538
|
+
FramesEncoded: result.framesEncoded,
|
|
539
|
+
DurationMs: Date.now() - started
|
|
540
|
+
};
|
|
541
|
+
} finally {
|
|
542
|
+
cleanupDir(work);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
310
545
|
async function uploadChunkOutput(storage, result, prefix, chunkIndex) {
|
|
311
546
|
const trimmed = trimTrailingSlash(prefix);
|
|
312
547
|
if (result.outputKind === "file") {
|
|
@@ -322,6 +557,9 @@ async function uploadChunkOutput(storage, result, prefix, chunkIndex) {
|
|
|
322
557
|
return uri;
|
|
323
558
|
}
|
|
324
559
|
async function handleAssemble(event, deps) {
|
|
560
|
+
if (event.PlanProtocol === "v2") {
|
|
561
|
+
return handleAssembleV2(event, deps);
|
|
562
|
+
}
|
|
325
563
|
const started = Date.now();
|
|
326
564
|
const storage = deps?.storage ?? getStorage();
|
|
327
565
|
const primitive = deps?.primitives?.assemble ?? assemble;
|
|
@@ -362,6 +600,91 @@ async function handleAssemble(event, deps) {
|
|
|
362
600
|
cleanupDir(work);
|
|
363
601
|
}
|
|
364
602
|
}
|
|
603
|
+
async function handleAssembleV2(event, deps) {
|
|
604
|
+
const started = Date.now();
|
|
605
|
+
const storage = deps?.storage ?? getStorage();
|
|
606
|
+
const primitive = deps?.primitives?.assemble ?? assemble;
|
|
607
|
+
const work = mkdtempSync(join(deps?.tmpRoot ?? tmpdir(), "hf-cr-assemble-v2-"));
|
|
608
|
+
try {
|
|
609
|
+
const planDir = await downloadAndMaterializePlanV2(storage, event, { role: "assembler" }, work);
|
|
610
|
+
const audioPath = existsSync3(join(planDir, "audio.aac")) ? join(planDir, "audio.aac") : null;
|
|
611
|
+
const chunkPaths = await downloadChunkObjects(storage, event.ChunkGcsUris, work, event.Format);
|
|
612
|
+
const finalOutput = event.Format === "png-sequence" ? join(work, "output-frames") : join(work, `output${formatExtension(event.Format)}`);
|
|
613
|
+
const result = await primitive(planDir, chunkPaths, audioPath, finalOutput, {
|
|
614
|
+
cfr: event.Cfr === true
|
|
615
|
+
});
|
|
616
|
+
if (event.Format === "png-sequence") {
|
|
617
|
+
const tarball = `${finalOutput}.tar.gz`;
|
|
618
|
+
await tarDirectory(finalOutput, tarball);
|
|
619
|
+
await uploadFileToGcs(storage, tarball, event.OutputGcsUri, "application/gzip");
|
|
620
|
+
} else {
|
|
621
|
+
await uploadFileToGcs(storage, finalOutput, event.OutputGcsUri);
|
|
622
|
+
}
|
|
623
|
+
return {
|
|
624
|
+
Action: "assemble",
|
|
625
|
+
OutputGcsUri: event.OutputGcsUri,
|
|
626
|
+
FramesEncoded: result.framesEncoded,
|
|
627
|
+
FileSize: result.fileSize,
|
|
628
|
+
DurationMs: Date.now() - started
|
|
629
|
+
};
|
|
630
|
+
} finally {
|
|
631
|
+
cleanupDir(work);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
async function downloadAndMaterializePlanV2(storage, event, target, work) {
|
|
635
|
+
const transportDir = join(work, "plan-v2");
|
|
636
|
+
mkdirSync2(transportDir, { recursive: true });
|
|
637
|
+
await downloadGcsObjectToFile(
|
|
638
|
+
storage,
|
|
639
|
+
event.PlanV2ManifestGcsUri,
|
|
640
|
+
join(transportDir, "plan.json")
|
|
641
|
+
);
|
|
642
|
+
const manifest = readPlanV2Manifest(transportDir);
|
|
643
|
+
if (manifest.planHash !== event.PlanHash) {
|
|
644
|
+
throwPlanHashMismatch(event.PlanHash, manifest.planHash);
|
|
645
|
+
}
|
|
646
|
+
const artifacts = listPlanV2ArtifactsForTarget(manifest, target);
|
|
647
|
+
const uniqueArtifacts = [
|
|
648
|
+
...new Map(artifacts.map((artifact) => [artifact.sha256, artifact])).values()
|
|
649
|
+
];
|
|
650
|
+
await mapConcurrent(uniqueArtifacts, 16, async (artifact) => {
|
|
651
|
+
await downloadPlanV2Artifact(storage, event.PlanV2ArtifactGcsPrefix, transportDir, artifact);
|
|
652
|
+
});
|
|
653
|
+
const planDir = join(work, "plan");
|
|
654
|
+
materializePlanV2Target(transportDir, target, planDir);
|
|
655
|
+
return planDir;
|
|
656
|
+
}
|
|
657
|
+
async function downloadPlanV2Artifact(storage, artifactPrefix, planV2Dir, artifact) {
|
|
658
|
+
await downloadGcsObjectToFileVerified(
|
|
659
|
+
storage,
|
|
660
|
+
planV2BlobUri(artifactPrefix, artifact.sha256),
|
|
661
|
+
planV2BlobPath(planV2Dir, artifact.sha256),
|
|
662
|
+
artifact.sha256
|
|
663
|
+
);
|
|
664
|
+
}
|
|
665
|
+
function planV2BlobPath(planV2Dir, digest) {
|
|
666
|
+
return join(planV2Dir, "artifacts", "sha256", digest.slice(0, 2), digest);
|
|
667
|
+
}
|
|
668
|
+
function planV2BlobUri(prefix, digest) {
|
|
669
|
+
return `${trimTrailingSlash(prefix)}/${digest.slice(0, 2)}/${digest}`;
|
|
670
|
+
}
|
|
671
|
+
function throwPlanHashMismatch(expected, actual) {
|
|
672
|
+
const error = new Error(
|
|
673
|
+
`PLAN_HASH_MISMATCH: event PlanHash=${expected} did not match v2 manifest planHash=${actual}`
|
|
674
|
+
);
|
|
675
|
+
error.name = "PLAN_HASH_MISMATCH";
|
|
676
|
+
throw error;
|
|
677
|
+
}
|
|
678
|
+
async function mapConcurrent(values, concurrency, fn) {
|
|
679
|
+
let cursor = 0;
|
|
680
|
+
async function worker() {
|
|
681
|
+
while (cursor < values.length) {
|
|
682
|
+
const index = cursor++;
|
|
683
|
+
await fn(values[index]);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, values.length) }, () => worker()));
|
|
687
|
+
}
|
|
365
688
|
async function downloadChunkObjects(storage, uris, workDir, format) {
|
|
366
689
|
const chunksDir = join(workDir, "chunks");
|
|
367
690
|
mkdirSync2(chunksDir, { recursive: true });
|
|
@@ -390,10 +713,10 @@ function getEventGcsUris(event) {
|
|
|
390
713
|
case "plan":
|
|
391
714
|
return [event.ProjectGcsUri, event.PlanOutputGcsPrefix];
|
|
392
715
|
case "renderChunk":
|
|
393
|
-
return [event.PlanGcsUri, event.ChunkOutputGcsPrefix];
|
|
716
|
+
return event.PlanProtocol === "v2" ? [event.PlanV2ManifestGcsUri, event.PlanV2ArtifactGcsPrefix, event.ChunkOutputGcsPrefix] : [event.PlanGcsUri, event.ChunkOutputGcsPrefix];
|
|
394
717
|
case "assemble":
|
|
395
718
|
return [
|
|
396
|
-
event.PlanGcsUri,
|
|
719
|
+
...event.PlanProtocol === "v2" ? [event.PlanV2ManifestGcsUri, event.PlanV2ArtifactGcsPrefix] : [event.PlanGcsUri],
|
|
397
720
|
...event.ChunkGcsUris,
|
|
398
721
|
event.OutputGcsUri,
|
|
399
722
|
event.AudioGcsUri
|
|
@@ -462,12 +785,17 @@ var NON_RETRYABLE_ERROR_NAMES = /* @__PURE__ */ new Set([
|
|
|
462
785
|
// Handler-boundary guards.
|
|
463
786
|
"GCS_URI_NOT_ALLOWED",
|
|
464
787
|
"PLAN_HASH_MISMATCH",
|
|
788
|
+
"PLAN_ARTIFACT_DIGEST_MISMATCH",
|
|
789
|
+
"PLAN_PROTOCOL_UNSUPPORTED",
|
|
790
|
+
"PLAN_V2_INTEGRITY_UNRECOVERABLE",
|
|
465
791
|
// Producer error class names (`.name`) + their string code aliases — the
|
|
466
792
|
// class sets `.name` to the class name but wraps a `code`; cover both so a
|
|
467
793
|
// raw-code throw is caught too. Mirrors the AWS state machine's
|
|
468
794
|
// non-retryable list.
|
|
469
795
|
"FormatNotSupportedInDistributedError",
|
|
470
796
|
"PlanTooLargeError",
|
|
797
|
+
"PlanProtocolUnsupportedError",
|
|
798
|
+
"PlanV2IntegrityError",
|
|
471
799
|
"RenderChunkValidationError",
|
|
472
800
|
"FFMPEG_VERSION_MISMATCH",
|
|
473
801
|
"FORMAT_NOT_SUPPORTED_IN_DISTRIBUTED",
|