@hyperframes/gcp-cloud-run 0.8.231 → 0.9.1
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/README.md +11 -16
- package/dist/events.d.ts +14 -75
- package/dist/index.d.ts +1 -1
- package/dist/index.js +30 -168
- package/dist/sdk/index.js +11 -1
- package/dist/sdk/renderToCloudRun.d.ts +1 -6
- package/dist/server.d.ts +1 -2
- package/dist/server.js +16 -164
- package/package.json +2 -2
- package/terraform/smoke-safety.test.ts +4 -6
- package/terraform/workflow.test.ts +34 -108
- package/terraform/workflow.yaml +27 -168
package/README.md
CHANGED
|
@@ -30,8 +30,8 @@ GCS bucket ←→ Cloud Run service (plan / renderChunk / assemble)
|
|
|
30
30
|
Cloud Workflows (Plan → parallel RenderChunk → Assemble)
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
-
- **Plan** downloads the project tarball and publishes
|
|
34
|
-
|
|
33
|
+
- **Plan** downloads the project tarball and publishes a Plan v2 manifest plus
|
|
34
|
+
content-addressed artifacts.
|
|
35
35
|
- **RenderChunk** runs in a parallel `for` loop in the workflow, fanned out
|
|
36
36
|
up to the plan's chunk count. Each invocation renders one chunk and uploads
|
|
37
37
|
it.
|
|
@@ -43,11 +43,11 @@ The workflow accumulates each step's small result body and returns
|
|
|
43
43
|
`{ Plan, Chunks, Assemble }` so `getRenderProgress` can read frame totals and
|
|
44
44
|
per-step durations on success.
|
|
45
45
|
|
|
46
|
-
### Plan transport
|
|
46
|
+
### Plan transport
|
|
47
47
|
|
|
48
|
-
Plan v2 is the
|
|
49
|
-
`renderToCloudRun` sends
|
|
50
|
-
|
|
48
|
+
Plan v2 is the only supported distributed plan transport.
|
|
49
|
+
`renderToCloudRun` sends `PlanProtocol: "v2"` without requiring a protocol
|
|
50
|
+
option:
|
|
51
51
|
|
|
52
52
|
```ts
|
|
53
53
|
await renderToCloudRun({
|
|
@@ -56,22 +56,17 @@ await renderToCloudRun({
|
|
|
56
56
|
```
|
|
57
57
|
|
|
58
58
|
V2 uses separate manifest and content-addressed artifact locators throughout
|
|
59
|
-
the workflow. Unknown protocols and integrity failures fail closed
|
|
60
|
-
never mixes v1 and v2 artifacts.
|
|
61
|
-
|
|
62
|
-
The monolithic v1 transport remains available as deprecated compatibility by
|
|
63
|
-
passing `planProtocol: "v1"` explicitly.
|
|
59
|
+
the workflow. Unknown protocols and integrity failures fail closed.
|
|
64
60
|
|
|
65
61
|
#### Upgrade order
|
|
66
62
|
|
|
67
63
|
Redeploy the Cloud Run image and Cloud Workflows definition from the same new
|
|
68
64
|
package version before upgrading an application that calls
|
|
69
65
|
`renderToCloudRun`. Pause new renders and drain active workflow executions
|
|
70
|
-
during the infrastructure update. Older workflows
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
complete.
|
|
66
|
+
during the infrastructure update. Older workflows may lack the Plan v2-only
|
|
67
|
+
request and locator contract. If infrastructure cannot be redeployed first,
|
|
68
|
+
keep the application on its previous package version until the workflow
|
|
69
|
+
redeploy is complete.
|
|
75
70
|
|
|
76
71
|
## Chrome runtime
|
|
77
72
|
|
package/dist/events.d.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* dispatches on the `Action` field of the JSON request body. Each action
|
|
7
7
|
* maps 1:1 onto one of the three OSS distributed primitives:
|
|
8
8
|
*
|
|
9
|
-
* "plan" → `
|
|
9
|
+
* "plan" → `planV2WithPublisher(...)` (Activity A)
|
|
10
10
|
* "renderChunk" → `renderChunk(planDir, chunkIndex, output)` (Activity B)
|
|
11
11
|
* "assemble" → `assemble(planDir, chunkPaths, audio, out)` (Activity C)
|
|
12
12
|
*
|
|
@@ -25,8 +25,6 @@ import type { DistributedFormat, SerializableDistributedRenderConfig } from "@hy
|
|
|
25
25
|
export type { SerializableDistributedRenderConfig } from "@hyperframes/producer/distributed";
|
|
26
26
|
/** Discriminator for the three roles the one Cloud Run image fulfills. */
|
|
27
27
|
export type CloudRunAction = "plan" | "renderChunk" | "assemble";
|
|
28
|
-
/** Transport protocol selected for one complete distributed render. */
|
|
29
|
-
export type CloudRunPlanProtocol = "v1" | "v2";
|
|
30
28
|
/**
|
|
31
29
|
* Top-level shape of any request body the handler may receive.
|
|
32
30
|
*
|
|
@@ -41,7 +39,7 @@ export type CloudRunEvent = PlanEvent | RenderChunkEvent | AssembleEvent | {
|
|
|
41
39
|
Input: CloudRunEvent;
|
|
42
40
|
};
|
|
43
41
|
/** Activity A: produce a planDir, upload to GCS. */
|
|
44
|
-
interface
|
|
42
|
+
export interface PlanEvent {
|
|
45
43
|
Action: "plan";
|
|
46
44
|
/** GCS URI pointing at a `tar -czf`-archived project directory (`gs://bucket/key.tar.gz`). */
|
|
47
45
|
ProjectGcsUri: string;
|
|
@@ -49,22 +47,13 @@ interface PlanEventBase {
|
|
|
49
47
|
PlanOutputGcsPrefix: string;
|
|
50
48
|
/** `DistributedRenderConfig` minus runtime-only fields (logger, abortSignal). */
|
|
51
49
|
Config: SerializableDistributedRenderConfig;
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Legacy plan transport. Callers must select it explicitly.
|
|
55
|
-
*
|
|
56
|
-
* @deprecated Use {@link PlanV2Event} for new integrations.
|
|
57
|
-
*/
|
|
58
|
-
export interface PlanV1Event extends PlanEventBase {
|
|
59
|
-
PlanProtocol: "v1";
|
|
60
|
-
}
|
|
61
|
-
/** Default content-addressed v2 plan transport. */
|
|
62
|
-
export interface PlanV2Event extends PlanEventBase {
|
|
50
|
+
/** Optional for direct handler callers; the SDK and orchestrator send it explicitly. */
|
|
63
51
|
PlanProtocol?: "v2";
|
|
64
52
|
}
|
|
65
|
-
|
|
53
|
+
/** Compatibility alias for the only supported plan event. */
|
|
54
|
+
export type PlanV2Event = PlanEvent;
|
|
66
55
|
/** Activity B: fetch planDir, render one chunk, upload result. */
|
|
67
|
-
interface
|
|
56
|
+
export interface RenderChunkEvent {
|
|
68
57
|
Action: "renderChunk";
|
|
69
58
|
/**
|
|
70
59
|
* `PlanResult.planHash` from the Plan invocation. The handler verifies
|
|
@@ -80,32 +69,14 @@ interface RenderChunkEventBase {
|
|
|
80
69
|
ChunkOutputGcsPrefix: string;
|
|
81
70
|
/** Output container format from the plan's encoder.json; drives file vs frame-dir handling. */
|
|
82
71
|
Format: DistributedFormat;
|
|
83
|
-
}
|
|
84
|
-
/**
|
|
85
|
-
* Legacy chunk event. Callers must select it explicitly.
|
|
86
|
-
*
|
|
87
|
-
* @deprecated Use {@link RenderChunkV2Event} for new integrations.
|
|
88
|
-
*/
|
|
89
|
-
export interface RenderChunkV1Event extends RenderChunkEventBase {
|
|
90
|
-
PlanProtocol: "v1";
|
|
91
|
-
/** GCS URI of the v1 plan tar produced by a PlanEvent invocation. */
|
|
92
|
-
PlanGcsUri: string;
|
|
93
|
-
PlanV2ManifestGcsUri?: never;
|
|
94
|
-
PlanV2ArtifactGcsPrefix?: never;
|
|
95
|
-
}
|
|
96
|
-
/**
|
|
97
|
-
* V2 chunk event. It intentionally cannot carry `PlanGcsUri`: the manifest
|
|
98
|
-
* describes the exact content-addressed artifacts needed by this chunk.
|
|
99
|
-
*/
|
|
100
|
-
export interface RenderChunkV2Event extends RenderChunkEventBase {
|
|
101
72
|
PlanProtocol?: "v2";
|
|
102
73
|
PlanV2ManifestGcsUri: string;
|
|
103
74
|
PlanV2ArtifactGcsPrefix: string;
|
|
104
|
-
PlanGcsUri?: never;
|
|
105
75
|
}
|
|
106
|
-
|
|
76
|
+
/** Compatibility alias for the only supported chunk event. */
|
|
77
|
+
export type RenderChunkV2Event = RenderChunkEvent;
|
|
107
78
|
/** Activity C: fetch planDir + all chunks + audio, assemble, upload final. */
|
|
108
|
-
interface
|
|
79
|
+
export interface AssembleEvent {
|
|
109
80
|
Action: "assemble";
|
|
110
81
|
/** GCS URIs of every chunk, ordered by chunk index. Length must equal `chunkCount`. */
|
|
111
82
|
ChunkGcsUris: string[];
|
|
@@ -123,34 +94,17 @@ interface AssembleEventBase {
|
|
|
123
94
|
* unset preserves current `-c copy` behavior.
|
|
124
95
|
*/
|
|
125
96
|
Cfr?: boolean;
|
|
126
|
-
}
|
|
127
|
-
/**
|
|
128
|
-
* Legacy assemble event. Callers must select it explicitly.
|
|
129
|
-
*
|
|
130
|
-
* @deprecated Use {@link AssembleV2Event} for new integrations.
|
|
131
|
-
*/
|
|
132
|
-
export interface AssembleV1Event extends AssembleEventBase {
|
|
133
|
-
PlanProtocol: "v1";
|
|
134
|
-
/** GCS URI of the v1 plan tar produced by a PlanEvent invocation. */
|
|
135
|
-
PlanGcsUri: string;
|
|
136
|
-
/** Legacy standalone audio locator; `null` when audio is embedded in the v1 plan tar. */
|
|
137
|
-
AudioGcsUri: string | null;
|
|
138
|
-
PlanV2ManifestGcsUri?: never;
|
|
139
|
-
PlanV2ArtifactGcsPrefix?: never;
|
|
140
|
-
}
|
|
141
|
-
/** V2 assemble event, scoped to manifest-declared assembler artifacts. */
|
|
142
|
-
export interface AssembleV2Event extends AssembleEventBase {
|
|
143
97
|
PlanProtocol?: "v2";
|
|
144
98
|
PlanV2ManifestGcsUri: string;
|
|
145
99
|
PlanV2ArtifactGcsPrefix: string;
|
|
146
100
|
PlanHash: string;
|
|
147
|
-
PlanGcsUri?: never;
|
|
148
101
|
/** V2 audio is a manifest artifact materialized only for the assembler. */
|
|
149
102
|
AudioGcsUri: null;
|
|
150
103
|
}
|
|
151
|
-
|
|
104
|
+
/** Compatibility alias for the only supported assemble event. */
|
|
105
|
+
export type AssembleV2Event = AssembleEvent;
|
|
152
106
|
/** Result of a `plan` invocation. Carries enough to size the Map(N) state. */
|
|
153
|
-
interface
|
|
107
|
+
export interface PlanResultBody {
|
|
154
108
|
Action: "plan";
|
|
155
109
|
PlanHash: string;
|
|
156
110
|
ChunkCount: number;
|
|
@@ -160,31 +114,16 @@ interface PlanResultBodyBase {
|
|
|
160
114
|
Height: number;
|
|
161
115
|
Format: DistributedFormat;
|
|
162
116
|
HasAudio: boolean;
|
|
163
|
-
AudioGcsUri: string | null;
|
|
164
117
|
FfmpegVersion: string;
|
|
165
118
|
ProducerVersion: string;
|
|
166
119
|
DurationMs: number;
|
|
167
|
-
}
|
|
168
|
-
/**
|
|
169
|
-
* Existing v1 result. Kept unchanged for wire compatibility.
|
|
170
|
-
*
|
|
171
|
-
* @deprecated New integrations should consume {@link PlanV2ResultBody}.
|
|
172
|
-
*/
|
|
173
|
-
export interface PlanV1ResultBody extends PlanResultBodyBase {
|
|
174
|
-
PlanGcsUri: string;
|
|
175
|
-
PlanProtocol?: never;
|
|
176
|
-
PlanV2ManifestGcsUri?: never;
|
|
177
|
-
PlanV2ArtifactGcsPrefix?: never;
|
|
178
|
-
}
|
|
179
|
-
/** V2 result. The two v2 locators are never aliases for `PlanGcsUri`. */
|
|
180
|
-
export interface PlanV2ResultBody extends PlanResultBodyBase {
|
|
181
120
|
PlanProtocol: "v2";
|
|
182
121
|
PlanV2ManifestGcsUri: string;
|
|
183
122
|
PlanV2ArtifactGcsPrefix: string;
|
|
184
|
-
PlanGcsUri?: never;
|
|
185
123
|
AudioGcsUri: null;
|
|
186
124
|
}
|
|
187
|
-
|
|
125
|
+
/** Compatibility alias for the only supported plan result. */
|
|
126
|
+
export type PlanV2ResultBody = PlanResultBody;
|
|
188
127
|
/** Result of a `renderChunk` invocation. Sized ≤200 bytes. */
|
|
189
128
|
export interface RenderChunkResultBody {
|
|
190
129
|
Action: "renderChunk";
|
package/dist/index.d.ts
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* separately.
|
|
19
19
|
*/
|
|
20
20
|
export { createApp, dispatch, type HandlerDeps, startServer, unwrapEvent } from "./server.js";
|
|
21
|
-
export { type AssembleEvent, type
|
|
21
|
+
export { type AssembleEvent, type AssembleV2Event, type AssembleResultBody, type CloudRunAction, type CloudRunEvent, type CloudRunResult, type PlanEvent, type PlanResultBody, type PlanV2Event, type PlanV2ResultBody, type RenderChunkEvent, type RenderChunkResultBody, type RenderChunkV2Event, type SerializableDistributedRenderConfig, } from "./events.js";
|
|
22
22
|
export { ChromeBinaryUnavailableError, resolveChromeExecutablePath } from "./chromium.js";
|
|
23
23
|
export { downloadGcsObjectToFile, downloadGcsObjectToFileVerified, formatGcsUri, type GcsLocation, parseGcsUri, sha256File, tarDirectory, untarDirectory, uploadContentAddressedFileToGcs, uploadFileToGcs, } from "./gcsTransport.js";
|
|
24
24
|
export { GcsPlanV2ArtifactPublisher, type GcsPlanV2ArtifactPublisherOptions, } from "./gcsPlanV2Publisher.js";
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/server.ts
|
|
2
|
-
import {
|
|
2
|
+
import { mkdirSync as mkdirSync3, mkdtempSync as mkdtempSync2, rmSync as rmSync3 } from "node:fs";
|
|
3
3
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
4
4
|
import { basename, extname, join as join2 } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
@@ -10,9 +10,7 @@ import {
|
|
|
10
10
|
assemble,
|
|
11
11
|
listPlanV2ArtifactsForTarget,
|
|
12
12
|
materializePlanV2Target,
|
|
13
|
-
plan,
|
|
14
13
|
isPlanAudioArtifactPath,
|
|
15
|
-
PLAN_AUDIO_RELATIVE_PATH,
|
|
16
14
|
resolvePlanAudioPath,
|
|
17
15
|
planV2WithPublisher,
|
|
18
16
|
readPlanV2Manifest,
|
|
@@ -383,27 +381,26 @@ async function dispatch(event, deps) {
|
|
|
383
381
|
function validatePlanProtocolShape(event) {
|
|
384
382
|
const raw = event;
|
|
385
383
|
const protocol = raw.PlanProtocol;
|
|
386
|
-
if (protocol !== void 0 && protocol !== "
|
|
384
|
+
if (protocol !== void 0 && protocol !== "v2") {
|
|
387
385
|
const error = new Error(
|
|
388
|
-
`[handler] unsupported PlanProtocol ${JSON.stringify(protocol)}; expected "
|
|
386
|
+
`[handler] unsupported PlanProtocol ${JSON.stringify(protocol)}; expected "v2" or absent`
|
|
389
387
|
);
|
|
390
388
|
error.name = "PLAN_PROTOCOL_UNSUPPORTED";
|
|
391
389
|
throw error;
|
|
392
390
|
}
|
|
393
391
|
if (event.Action === "plan") return;
|
|
394
|
-
const effectiveProtocol = protocol ?? "v2";
|
|
395
392
|
const hasV1Locator = typeof raw.PlanGcsUri === "string";
|
|
396
393
|
const hasV2Manifest = typeof raw.PlanV2ManifestGcsUri === "string";
|
|
397
394
|
const hasV2Prefix = typeof raw.PlanV2ArtifactGcsPrefix === "string";
|
|
398
|
-
const valid =
|
|
395
|
+
const valid = !hasV1Locator && hasV2Manifest && hasV2Prefix;
|
|
399
396
|
if (!valid) {
|
|
400
397
|
const error = new Error(
|
|
401
|
-
`[handler]
|
|
398
|
+
`[handler] v2 ${event.Action} event has mixed or missing plan locators`
|
|
402
399
|
);
|
|
403
400
|
error.name = "PLAN_PROTOCOL_UNSUPPORTED";
|
|
404
401
|
throw error;
|
|
405
402
|
}
|
|
406
|
-
if (
|
|
403
|
+
if (event.Action === "assemble" && event.AudioGcsUri !== null) {
|
|
407
404
|
const error = new Error("[handler] v2 assemble audio must be materialized from the manifest");
|
|
408
405
|
error.name = "PLAN_PROTOCOL_UNSUPPORTED";
|
|
409
406
|
throw error;
|
|
@@ -412,7 +409,7 @@ function validatePlanProtocolShape(event) {
|
|
|
412
409
|
function normalizeTerminalErrorName(error) {
|
|
413
410
|
if (!error || typeof error !== "object") return;
|
|
414
411
|
const candidate = error;
|
|
415
|
-
if (candidate.code === "PLAN_PROTOCOL_UNSUPPORTED" || candidate.code === "
|
|
412
|
+
if (candidate.code === "PLAN_PROTOCOL_UNSUPPORTED" || candidate.code === "PLAN_V2_INTEGRITY_UNRECOVERABLE" || candidate.code === "FONT_FETCH_FAILED" || candidate.code === "FONT_FETCH_UNAVAILABLE" || candidate.code === "VIDEO_SOURCE_UNRENDERABLE" || candidate.code === "VIDEO_EXTRACTION_FAILED" || candidate.code === "INVALID_VIDEO_METADATA" || candidate.code === "CHUNK_EXCEEDS_WORKER_TIMEOUT") {
|
|
416
413
|
candidate.name = candidate.code;
|
|
417
414
|
}
|
|
418
415
|
}
|
|
@@ -459,14 +456,14 @@ function summarizeEvent(event) {
|
|
|
459
456
|
case "renderChunk":
|
|
460
457
|
return {
|
|
461
458
|
planProtocol: event.PlanProtocol ?? "v2",
|
|
462
|
-
|
|
459
|
+
planV2ManifestGcsUri: event.PlanV2ManifestGcsUri,
|
|
463
460
|
chunkIndex: event.ChunkIndex,
|
|
464
461
|
format: event.Format
|
|
465
462
|
};
|
|
466
463
|
case "assemble":
|
|
467
464
|
return {
|
|
468
465
|
planProtocol: event.PlanProtocol ?? "v2",
|
|
469
|
-
|
|
466
|
+
planV2ManifestGcsUri: event.PlanV2ManifestGcsUri,
|
|
470
467
|
chunkCount: event.ChunkGcsUris.length,
|
|
471
468
|
hasAudio: event.AudioGcsUri !== null,
|
|
472
469
|
outputGcsUri: event.OutputGcsUri,
|
|
@@ -480,51 +477,6 @@ function primeChrome(deps) {
|
|
|
480
477
|
process.env.PRODUCER_HEADLESS_SHELL_PATH = resolveChromeExecutablePath();
|
|
481
478
|
}
|
|
482
479
|
async function handlePlan(event, deps) {
|
|
483
|
-
if (event.PlanProtocol !== "v1") {
|
|
484
|
-
return handlePlanV2(event, deps);
|
|
485
|
-
}
|
|
486
|
-
const started = Date.now();
|
|
487
|
-
const storage = deps?.storage ?? getStorage();
|
|
488
|
-
const primitive = deps?.primitives?.plan ?? plan;
|
|
489
|
-
primeChrome(deps);
|
|
490
|
-
const work = mkdtempSync2(join2(deps?.tmpRoot ?? tmpdir2(), "hf-cr-plan-"));
|
|
491
|
-
const projectArchive = join2(work, "project.tar.gz");
|
|
492
|
-
const projectDir = join2(work, "project");
|
|
493
|
-
const planDir = join2(work, "plan");
|
|
494
|
-
try {
|
|
495
|
-
await downloadGcsObjectToFile(storage, event.ProjectGcsUri, projectArchive);
|
|
496
|
-
await untarDirectory(projectArchive, projectDir);
|
|
497
|
-
const config = {
|
|
498
|
-
...event.Config
|
|
499
|
-
};
|
|
500
|
-
const result = await primitive(projectDir, config, planDir);
|
|
501
|
-
const planTar = join2(work, "plan.tar.gz");
|
|
502
|
-
await tarDirectory(planDir, planTar);
|
|
503
|
-
const planTarUri = `${trimTrailingSlash2(event.PlanOutputGcsPrefix)}/plan.tar.gz`;
|
|
504
|
-
const audioPath = join2(planDir, PLAN_AUDIO_RELATIVE_PATH);
|
|
505
|
-
const hasAudio = existsSync3(audioPath) && statSync3(audioPath).size > 0;
|
|
506
|
-
await uploadFileToGcs(storage, planTar, planTarUri, "application/gzip");
|
|
507
|
-
return {
|
|
508
|
-
Action: "plan",
|
|
509
|
-
PlanGcsUri: planTarUri,
|
|
510
|
-
PlanHash: result.planHash,
|
|
511
|
-
ChunkCount: result.chunkCount,
|
|
512
|
-
TotalFrames: result.totalFrames,
|
|
513
|
-
Fps: result.fps,
|
|
514
|
-
Width: result.width,
|
|
515
|
-
Height: result.height,
|
|
516
|
-
Format: result.format,
|
|
517
|
-
HasAudio: hasAudio,
|
|
518
|
-
AudioGcsUri: null,
|
|
519
|
-
FfmpegVersion: result.ffmpegVersion,
|
|
520
|
-
ProducerVersion: result.producerVersion,
|
|
521
|
-
DurationMs: Date.now() - started
|
|
522
|
-
};
|
|
523
|
-
} finally {
|
|
524
|
-
cleanupDir(work);
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
async function handlePlanV2(event, deps) {
|
|
528
480
|
const started = Date.now();
|
|
529
481
|
const storage = deps?.storage ?? getStorage();
|
|
530
482
|
const primitive = deps?.primitives?.planV2WithPublisher ?? planV2WithPublisher;
|
|
@@ -566,45 +518,6 @@ async function handlePlanV2(event, deps) {
|
|
|
566
518
|
}
|
|
567
519
|
}
|
|
568
520
|
async function handleRenderChunk(event, deps) {
|
|
569
|
-
if (event.PlanProtocol !== "v1") {
|
|
570
|
-
return handleRenderChunkV2(event, deps);
|
|
571
|
-
}
|
|
572
|
-
const started = Date.now();
|
|
573
|
-
const storage = deps?.storage ?? getStorage();
|
|
574
|
-
const primitive = deps?.primitives?.renderChunk ?? renderChunk;
|
|
575
|
-
primeChrome(deps);
|
|
576
|
-
const work = mkdtempSync2(join2(deps?.tmpRoot ?? tmpdir2(), "hf-cr-chunk-"));
|
|
577
|
-
const planTar = join2(work, "plan.tar.gz");
|
|
578
|
-
const planDir = join2(work, "plan");
|
|
579
|
-
try {
|
|
580
|
-
await downloadGcsObjectToFile(storage, event.PlanGcsUri, planTar);
|
|
581
|
-
await untarDirectory(planTar, planDir);
|
|
582
|
-
verifyPlanHash(planDir, event.PlanHash);
|
|
583
|
-
const chunkOutputBase = join2(
|
|
584
|
-
work,
|
|
585
|
-
event.Format === "png-sequence" ? `chunk-${pad(event.ChunkIndex)}` : `chunk-${pad(event.ChunkIndex)}${formatExtension(event.Format)}`
|
|
586
|
-
);
|
|
587
|
-
const result = await primitive(planDir, event.ChunkIndex, chunkOutputBase);
|
|
588
|
-
const chunkUri = await uploadChunkOutput(
|
|
589
|
-
storage,
|
|
590
|
-
result,
|
|
591
|
-
event.ChunkOutputGcsPrefix,
|
|
592
|
-
event.ChunkIndex
|
|
593
|
-
);
|
|
594
|
-
return {
|
|
595
|
-
Action: "renderChunk",
|
|
596
|
-
ChunkGcsUri: chunkUri,
|
|
597
|
-
ChunkIndex: event.ChunkIndex,
|
|
598
|
-
Sha256: result.sha256,
|
|
599
|
-
FramesEncoded: result.framesEncoded,
|
|
600
|
-
CaptureMode: result.captureMode,
|
|
601
|
-
DurationMs: Date.now() - started
|
|
602
|
-
};
|
|
603
|
-
} finally {
|
|
604
|
-
cleanupDir(work);
|
|
605
|
-
}
|
|
606
|
-
}
|
|
607
|
-
async function handleRenderChunkV2(event, deps) {
|
|
608
521
|
const started = Date.now();
|
|
609
522
|
const storage = deps?.storage ?? getStorage();
|
|
610
523
|
const primitive = deps?.primitives?.renderChunk ?? renderChunk;
|
|
@@ -656,50 +569,6 @@ async function uploadChunkOutput(storage, result, prefix, chunkIndex) {
|
|
|
656
569
|
return uri;
|
|
657
570
|
}
|
|
658
571
|
async function handleAssemble(event, deps) {
|
|
659
|
-
if (event.PlanProtocol !== "v1") {
|
|
660
|
-
return handleAssembleV2(event, deps);
|
|
661
|
-
}
|
|
662
|
-
const started = Date.now();
|
|
663
|
-
const storage = deps?.storage ?? getStorage();
|
|
664
|
-
const primitive = deps?.primitives?.assemble ?? assemble;
|
|
665
|
-
const work = mkdtempSync2(join2(deps?.tmpRoot ?? tmpdir2(), "hf-cr-assemble-"));
|
|
666
|
-
const planTar = join2(work, "plan.tar.gz");
|
|
667
|
-
const planDir = join2(work, "plan");
|
|
668
|
-
try {
|
|
669
|
-
await downloadGcsObjectToFile(storage, event.PlanGcsUri, planTar);
|
|
670
|
-
await untarDirectory(planTar, planDir);
|
|
671
|
-
const chunkPaths = await downloadChunkObjects(storage, event.ChunkGcsUris, work, event.Format);
|
|
672
|
-
let audioPath = null;
|
|
673
|
-
const planAudio = resolvePlanAudioPath(planDir) ?? join2(planDir, PLAN_AUDIO_RELATIVE_PATH);
|
|
674
|
-
if (existsSync3(planAudio) && statSync3(planAudio).size > 0) {
|
|
675
|
-
audioPath = planAudio;
|
|
676
|
-
} else if (event.AudioGcsUri) {
|
|
677
|
-
audioPath = planAudio;
|
|
678
|
-
await downloadGcsObjectToFile(storage, event.AudioGcsUri, audioPath);
|
|
679
|
-
}
|
|
680
|
-
const finalOutput = event.Format === "png-sequence" ? join2(work, "output-frames") : join2(work, `output${formatExtension(event.Format)}`);
|
|
681
|
-
const result = await primitive(planDir, chunkPaths, audioPath, finalOutput, {
|
|
682
|
-
cfr: event.Cfr === true
|
|
683
|
-
});
|
|
684
|
-
if (event.Format === "png-sequence") {
|
|
685
|
-
const tarball = `${finalOutput}.tar.gz`;
|
|
686
|
-
await tarDirectory(finalOutput, tarball);
|
|
687
|
-
await uploadFileToGcs(storage, tarball, event.OutputGcsUri, "application/gzip");
|
|
688
|
-
} else {
|
|
689
|
-
await uploadFileToGcs(storage, finalOutput, event.OutputGcsUri);
|
|
690
|
-
}
|
|
691
|
-
return {
|
|
692
|
-
Action: "assemble",
|
|
693
|
-
OutputGcsUri: event.OutputGcsUri,
|
|
694
|
-
FramesEncoded: result.framesEncoded,
|
|
695
|
-
FileSize: result.fileSize,
|
|
696
|
-
DurationMs: Date.now() - started
|
|
697
|
-
};
|
|
698
|
-
} finally {
|
|
699
|
-
cleanupDir(work);
|
|
700
|
-
}
|
|
701
|
-
}
|
|
702
|
-
async function handleAssembleV2(event, deps) {
|
|
703
572
|
const started = Date.now();
|
|
704
573
|
const storage = deps?.storage ?? getStorage();
|
|
705
574
|
const primitive = deps?.primitives?.assemble ?? assemble;
|
|
@@ -818,10 +687,15 @@ function getEventGcsUris(event) {
|
|
|
818
687
|
case "plan":
|
|
819
688
|
return [event.ProjectGcsUri, event.PlanOutputGcsPrefix];
|
|
820
689
|
case "renderChunk":
|
|
821
|
-
return
|
|
690
|
+
return [
|
|
691
|
+
event.PlanV2ManifestGcsUri,
|
|
692
|
+
event.PlanV2ArtifactGcsPrefix,
|
|
693
|
+
event.ChunkOutputGcsPrefix
|
|
694
|
+
];
|
|
822
695
|
case "assemble":
|
|
823
696
|
return [
|
|
824
|
-
|
|
697
|
+
event.PlanV2ManifestGcsUri,
|
|
698
|
+
event.PlanV2ArtifactGcsPrefix,
|
|
825
699
|
...event.ChunkGcsUris,
|
|
826
700
|
event.OutputGcsUri,
|
|
827
701
|
event.AudioGcsUri
|
|
@@ -866,26 +740,6 @@ function cleanupDir(dir) {
|
|
|
866
740
|
} catch {
|
|
867
741
|
}
|
|
868
742
|
}
|
|
869
|
-
function verifyPlanHash(planDir, expected) {
|
|
870
|
-
const planJsonPath = join2(planDir, "plan.json");
|
|
871
|
-
let parsed;
|
|
872
|
-
try {
|
|
873
|
-
parsed = JSON.parse(readFileSync(planJsonPath, "utf-8"));
|
|
874
|
-
} catch (err) {
|
|
875
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
876
|
-
const error = new Error(`PLAN_HASH_MISMATCH: failed to read ${planJsonPath}: ${msg}`);
|
|
877
|
-
error.name = "PLAN_HASH_MISMATCH";
|
|
878
|
-
throw error;
|
|
879
|
-
}
|
|
880
|
-
const actual = parsed.planHash;
|
|
881
|
-
if (typeof actual !== "string" || actual !== expected) {
|
|
882
|
-
const error = new Error(
|
|
883
|
-
`PLAN_HASH_MISMATCH: event PlanHash=${expected} did not match plan.json planHash=${String(actual)}`
|
|
884
|
-
);
|
|
885
|
-
error.name = "PLAN_HASH_MISMATCH";
|
|
886
|
-
throw error;
|
|
887
|
-
}
|
|
888
|
-
}
|
|
889
743
|
var NON_RETRYABLE_ERROR_NAMES = /* @__PURE__ */ new Set([
|
|
890
744
|
// Handler-boundary guards.
|
|
891
745
|
"GCS_URI_NOT_ALLOWED",
|
|
@@ -903,13 +757,11 @@ var NON_RETRYABLE_ERROR_NAMES = /* @__PURE__ */ new Set([
|
|
|
903
757
|
// non-retryable list.
|
|
904
758
|
"FormatNotSupportedInDistributedError",
|
|
905
759
|
"NotMediaPayloadError",
|
|
906
|
-
"PlanTooLargeError",
|
|
907
760
|
"PlanProtocolUnsupportedError",
|
|
908
761
|
"PlanV2IntegrityError",
|
|
909
762
|
"RenderChunkValidationError",
|
|
910
763
|
"FFMPEG_VERSION_MISMATCH",
|
|
911
764
|
"FORMAT_NOT_SUPPORTED_IN_DISTRIBUTED",
|
|
912
|
-
"PLAN_TOO_LARGE",
|
|
913
765
|
"BROWSER_GPU_NOT_SOFTWARE",
|
|
914
766
|
"FONT_FETCH_FAILED",
|
|
915
767
|
"ChromeBinaryUnavailableError"
|
|
@@ -948,13 +800,13 @@ if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
|
|
|
948
800
|
}
|
|
949
801
|
|
|
950
802
|
// src/sdk/deploySite.ts
|
|
951
|
-
import { mkdtempSync as mkdtempSync3, rmSync as rmSync4, statSync as
|
|
803
|
+
import { mkdtempSync as mkdtempSync3, rmSync as rmSync4, statSync as statSync3 } from "node:fs";
|
|
952
804
|
import { tmpdir as tmpdir3 } from "node:os";
|
|
953
805
|
import { join as join3 } from "node:path";
|
|
954
806
|
import { Storage as Storage2 } from "@google-cloud/storage";
|
|
955
807
|
import { hashProjectDir, listSiteFiles } from "@hyperframes/producer/distributed";
|
|
956
808
|
async function deploySite(opts) {
|
|
957
|
-
if (!
|
|
809
|
+
if (!statSync3(opts.projectDir).isDirectory()) {
|
|
958
810
|
throw new Error(`[deploySite] projectDir is not a directory: ${opts.projectDir}`);
|
|
959
811
|
}
|
|
960
812
|
const files = listSiteFiles(opts.projectDir);
|
|
@@ -978,7 +830,7 @@ async function deploySite(opts) {
|
|
|
978
830
|
try {
|
|
979
831
|
const tarball = join3(workdir, "project.tar.gz");
|
|
980
832
|
await tarDirectory(opts.projectDir, tarball, files);
|
|
981
|
-
const size =
|
|
833
|
+
const size = statSync3(tarball).size;
|
|
982
834
|
await uploadFileToGcs(storage, tarball, projectGcsUri, "application/gzip");
|
|
983
835
|
return {
|
|
984
836
|
siteId,
|
|
@@ -1042,7 +894,17 @@ function validateWorkflowsInputSize(input) {
|
|
|
1042
894
|
}
|
|
1043
895
|
|
|
1044
896
|
// src/sdk/renderToCloudRun.ts
|
|
897
|
+
function rejectUnsupportedLegacyPlanProtocol(opts) {
|
|
898
|
+
const planProtocol = Reflect.get(opts, "planProtocol");
|
|
899
|
+
if (planProtocol === void 0 || planProtocol === "v2") return;
|
|
900
|
+
const error = new Error(
|
|
901
|
+
`[renderToCloudRun] planProtocol is no longer configurable; expected "v2" or absent, got ${JSON.stringify(planProtocol)}`
|
|
902
|
+
);
|
|
903
|
+
error.name = "PLAN_PROTOCOL_UNSUPPORTED";
|
|
904
|
+
throw error;
|
|
905
|
+
}
|
|
1045
906
|
async function renderToCloudRun(opts) {
|
|
907
|
+
rejectUnsupportedLegacyPlanProtocol(opts);
|
|
1046
908
|
validateDistributedRenderConfig(opts.config);
|
|
1047
909
|
if (!opts.bucketName) throw new Error("[renderToCloudRun] bucketName is required");
|
|
1048
910
|
if (!opts.projectId) throw new Error("[renderToCloudRun] projectId is required");
|
|
@@ -1077,7 +939,7 @@ async function renderToCloudRun(opts) {
|
|
|
1077
939
|
OutputGcsUri: outputGcsUri,
|
|
1078
940
|
ServiceUrl: opts.serviceUrl,
|
|
1079
941
|
Config: opts.config,
|
|
1080
|
-
PlanProtocol:
|
|
942
|
+
PlanProtocol: "v2"
|
|
1081
943
|
};
|
|
1082
944
|
validateWorkflowsInputSize(argument);
|
|
1083
945
|
const executions = opts.executions ?? await defaultExecutionsClient();
|
package/dist/sdk/index.js
CHANGED
|
@@ -161,7 +161,17 @@ function validateWorkflowsInputSize(input) {
|
|
|
161
161
|
}
|
|
162
162
|
|
|
163
163
|
// src/sdk/renderToCloudRun.ts
|
|
164
|
+
function rejectUnsupportedLegacyPlanProtocol(opts) {
|
|
165
|
+
const planProtocol = Reflect.get(opts, "planProtocol");
|
|
166
|
+
if (planProtocol === void 0 || planProtocol === "v2") return;
|
|
167
|
+
const error = new Error(
|
|
168
|
+
`[renderToCloudRun] planProtocol is no longer configurable; expected "v2" or absent, got ${JSON.stringify(planProtocol)}`
|
|
169
|
+
);
|
|
170
|
+
error.name = "PLAN_PROTOCOL_UNSUPPORTED";
|
|
171
|
+
throw error;
|
|
172
|
+
}
|
|
164
173
|
async function renderToCloudRun(opts) {
|
|
174
|
+
rejectUnsupportedLegacyPlanProtocol(opts);
|
|
165
175
|
validateDistributedRenderConfig(opts.config);
|
|
166
176
|
if (!opts.bucketName) throw new Error("[renderToCloudRun] bucketName is required");
|
|
167
177
|
if (!opts.projectId) throw new Error("[renderToCloudRun] projectId is required");
|
|
@@ -196,7 +206,7 @@ async function renderToCloudRun(opts) {
|
|
|
196
206
|
OutputGcsUri: outputGcsUri,
|
|
197
207
|
ServiceUrl: opts.serviceUrl,
|
|
198
208
|
Config: opts.config,
|
|
199
|
-
PlanProtocol:
|
|
209
|
+
PlanProtocol: "v2"
|
|
200
210
|
};
|
|
201
211
|
validateWorkflowsInputSize(argument);
|
|
202
212
|
const executions = opts.executions ?? await defaultExecutionsClient();
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
* separately for polling.
|
|
25
25
|
*/
|
|
26
26
|
import type { Storage } from "@google-cloud/storage";
|
|
27
|
-
import type {
|
|
27
|
+
import type { SerializableDistributedRenderConfig } from "../events.js";
|
|
28
28
|
import { type SiteHandle } from "./deploySite.js";
|
|
29
29
|
/**
|
|
30
30
|
* Minimal surface of `@google-cloud/workflows`' `ExecutionsClient` that
|
|
@@ -50,11 +50,6 @@ export interface RenderToCloudRunOptions {
|
|
|
50
50
|
siteHandle?: SiteHandle;
|
|
51
51
|
/** Validated `SerializableDistributedRenderConfig` (no logger / abortSignal). */
|
|
52
52
|
config: SerializableDistributedRenderConfig;
|
|
53
|
-
/**
|
|
54
|
-
* Distributed plan transport. Defaults to `"v2"`. Select `"v1"`
|
|
55
|
-
* explicitly only for deprecated compatibility with the monolithic plan.
|
|
56
|
-
*/
|
|
57
|
-
planProtocol?: CloudRunPlanProtocol;
|
|
58
53
|
/** GCS bucket from the Terraform output (`render_bucket_name`). */
|
|
59
54
|
bucketName: string;
|
|
60
55
|
/** GCP project id hosting the workflow. */
|
package/dist/server.d.ts
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import { Storage } from "@google-cloud/storage";
|
|
19
19
|
import { Hono } from "hono";
|
|
20
|
-
import { assemble, type ChunkRenderer,
|
|
20
|
+
import { assemble, type ChunkRenderer, planV2WithPublisher } from "@hyperframes/producer/distributed";
|
|
21
21
|
import type { AssembleEvent, CloudRunEvent, CloudRunResult, PlanEvent, RenderChunkEvent } from "./events.js";
|
|
22
22
|
/**
|
|
23
23
|
* Optional injection points used by the handler's unit tests. Production
|
|
@@ -27,7 +27,6 @@ import type { AssembleEvent, CloudRunEvent, CloudRunResult, PlanEvent, RenderChu
|
|
|
27
27
|
export interface HandlerDeps {
|
|
28
28
|
storage?: Storage;
|
|
29
29
|
primitives?: {
|
|
30
|
-
plan: typeof plan;
|
|
31
30
|
planV2WithPublisher?: typeof planV2WithPublisher;
|
|
32
31
|
renderChunk: ChunkRenderer;
|
|
33
32
|
assemble: typeof assemble;
|