@hyperframes/gcp-cloud-run 0.6.79
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 +118 -0
- package/README.md +128 -0
- package/dist/chromium.d.ts +39 -0
- package/dist/chromium.d.ts.map +1 -0
- package/dist/events.d.ts +130 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/formatExtension.d.ts +11 -0
- package/dist/formatExtension.d.ts.map +1 -0
- package/dist/gcsTransport.d.ts +53 -0
- package/dist/gcsTransport.d.ts.map +1 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +855 -0
- package/dist/index.js.map +7 -0
- package/dist/sdk/costAccounting.d.ts +67 -0
- package/dist/sdk/costAccounting.d.ts.map +1 -0
- package/dist/sdk/deploySite.d.ts +54 -0
- package/dist/sdk/deploySite.d.ts.map +1 -0
- package/dist/sdk/getRenderProgress.d.ts +91 -0
- package/dist/sdk/getRenderProgress.d.ts.map +1 -0
- package/dist/sdk/index.d.ts +17 -0
- package/dist/sdk/index.d.ts.map +1 -0
- package/dist/sdk/index.js +390 -0
- package/dist/sdk/index.js.map +7 -0
- package/dist/sdk/renderToCloudRun.d.ts +97 -0
- package/dist/sdk/renderToCloudRun.d.ts.map +1 -0
- package/dist/sdk/validateConfig.d.ts +36 -0
- package/dist/sdk/validateConfig.d.ts.map +1 -0
- package/dist/server.d.ts +58 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +517 -0
- package/dist/server.js.map +7 -0
- package/package.json +62 -0
- package/terraform/main.tf +197 -0
- package/terraform/outputs.tf +34 -0
- package/terraform/providers.tf +12 -0
- package/terraform/variables.tf +75 -0
- package/terraform/versions.tf +9 -0
- package/terraform/workflow.yaml +179 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,855 @@
|
|
|
1
|
+
// src/server.ts
|
|
2
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2, mkdtempSync, readFileSync, rmSync as rmSync2, statSync as statSync2 } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { basename, extname, join } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { serve } from "@hono/node-server";
|
|
7
|
+
import { Storage } from "@google-cloud/storage";
|
|
8
|
+
import { Hono } from "hono";
|
|
9
|
+
import {
|
|
10
|
+
assemble,
|
|
11
|
+
plan,
|
|
12
|
+
renderChunk
|
|
13
|
+
} from "@hyperframes/producer/distributed";
|
|
14
|
+
|
|
15
|
+
// src/chromium.ts
|
|
16
|
+
import { existsSync } from "node:fs";
|
|
17
|
+
var ChromeBinaryUnavailableError = class extends Error {
|
|
18
|
+
// Read indirectly via the error envelope / Error.prototype.toString.
|
|
19
|
+
// fallow-ignore-next-line unused-class-member
|
|
20
|
+
name = "ChromeBinaryUnavailableError";
|
|
21
|
+
resolvedPath;
|
|
22
|
+
constructor(resolvedPath, hint) {
|
|
23
|
+
super(`[chromium] Chrome binary unavailable: ${hint}`);
|
|
24
|
+
this.resolvedPath = resolvedPath;
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
var FALLBACK_CHROME_PATHS = [
|
|
28
|
+
"/opt/chrome/chrome-headless-shell",
|
|
29
|
+
"/usr/bin/chrome-headless-shell",
|
|
30
|
+
"/usr/bin/google-chrome",
|
|
31
|
+
"/usr/bin/google-chrome-stable",
|
|
32
|
+
"/usr/bin/chromium",
|
|
33
|
+
"/usr/bin/chromium-browser"
|
|
34
|
+
];
|
|
35
|
+
function resolveChromeExecutablePath() {
|
|
36
|
+
const fromEngineOverride = process.env.PRODUCER_HEADLESS_SHELL_PATH?.trim();
|
|
37
|
+
if (fromEngineOverride) {
|
|
38
|
+
if (!existsSync(fromEngineOverride)) {
|
|
39
|
+
throw new ChromeBinaryUnavailableError(
|
|
40
|
+
fromEngineOverride,
|
|
41
|
+
`PRODUCER_HEADLESS_SHELL_PATH=${JSON.stringify(fromEngineOverride)} does not exist on disk.`
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
return fromEngineOverride;
|
|
45
|
+
}
|
|
46
|
+
const fromImage = process.env.HYPERFRAMES_CHROME_PATH?.trim();
|
|
47
|
+
if (fromImage) {
|
|
48
|
+
if (!existsSync(fromImage)) {
|
|
49
|
+
throw new ChromeBinaryUnavailableError(
|
|
50
|
+
fromImage,
|
|
51
|
+
`HYPERFRAMES_CHROME_PATH=${JSON.stringify(fromImage)} does not exist on disk.`
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
return fromImage;
|
|
55
|
+
}
|
|
56
|
+
for (const candidate of FALLBACK_CHROME_PATHS) {
|
|
57
|
+
if (existsSync(candidate)) return candidate;
|
|
58
|
+
}
|
|
59
|
+
throw new ChromeBinaryUnavailableError(
|
|
60
|
+
null,
|
|
61
|
+
`no Chrome binary found. Set HYPERFRAMES_CHROME_PATH (the Dockerfile does this) or PRODUCER_HEADLESS_SHELL_PATH to the absolute path of a chrome-headless-shell binary. Searched: ${FALLBACK_CHROME_PATHS.join(", ")}.`
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// src/formatExtension.ts
|
|
66
|
+
var FORMAT_EXTENSIONS = {
|
|
67
|
+
mp4: ".mp4",
|
|
68
|
+
mov: ".mov",
|
|
69
|
+
webm: ".webm",
|
|
70
|
+
"png-sequence": ""
|
|
71
|
+
};
|
|
72
|
+
function formatExtension(format) {
|
|
73
|
+
return FORMAT_EXTENSIONS[format];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// src/gcsTransport.ts
|
|
77
|
+
import { createWriteStream, existsSync as existsSync2, mkdirSync, rmSync, statSync } from "node:fs";
|
|
78
|
+
import { dirname } from "node:path";
|
|
79
|
+
import { pipeline } from "node:stream/promises";
|
|
80
|
+
import * as tar from "tar";
|
|
81
|
+
function parseGcsUri(uri) {
|
|
82
|
+
if (!uri.startsWith("gs://")) {
|
|
83
|
+
throw new Error(`[gcsTransport] expected gs:// URI, got: ${JSON.stringify(uri)}`);
|
|
84
|
+
}
|
|
85
|
+
const rest = uri.slice("gs://".length);
|
|
86
|
+
const slash = rest.indexOf("/");
|
|
87
|
+
if (slash === -1) {
|
|
88
|
+
throw new Error(`[gcsTransport] missing key in gs URI: ${JSON.stringify(uri)}`);
|
|
89
|
+
}
|
|
90
|
+
const bucket = rest.slice(0, slash);
|
|
91
|
+
const key = rest.slice(slash + 1);
|
|
92
|
+
if (!bucket || !key) {
|
|
93
|
+
throw new Error(`[gcsTransport] empty bucket or key in gs URI: ${JSON.stringify(uri)}`);
|
|
94
|
+
}
|
|
95
|
+
return { bucket, key };
|
|
96
|
+
}
|
|
97
|
+
function formatGcsUri(loc) {
|
|
98
|
+
return `gs://${loc.bucket}/${loc.key}`;
|
|
99
|
+
}
|
|
100
|
+
async function downloadGcsObjectToFile(storage, uri, destPath) {
|
|
101
|
+
const { bucket, key } = parseGcsUri(uri);
|
|
102
|
+
mkdirSync(dirname(destPath), { recursive: true });
|
|
103
|
+
const file = storage.bucket(bucket).file(key);
|
|
104
|
+
await pipeline(file.createReadStream(), createWriteStream(destPath));
|
|
105
|
+
}
|
|
106
|
+
async function uploadFileToGcs(storage, localPath, uri, contentType) {
|
|
107
|
+
if (!existsSync2(localPath)) {
|
|
108
|
+
throw new Error(`[gcsTransport] upload source missing: ${localPath}`);
|
|
109
|
+
}
|
|
110
|
+
const { bucket, key } = parseGcsUri(uri);
|
|
111
|
+
await storage.bucket(bucket).upload(localPath, {
|
|
112
|
+
destination: key,
|
|
113
|
+
// `resumable: false` (simple upload) is faster for the small-to-medium
|
|
114
|
+
// objects this adapter moves and avoids the extra round-trip a resumable
|
|
115
|
+
// session start costs; GCS recommends resumable only past ~8 MB but our
|
|
116
|
+
// chunks are reliably above that, so let the client pick by default.
|
|
117
|
+
contentType
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
async function tarDirectory(sourceDir, destTarball) {
|
|
121
|
+
if (!existsSync2(sourceDir) || !statSync(sourceDir).isDirectory()) {
|
|
122
|
+
throw new Error(`[gcsTransport] tar source must be an existing directory: ${sourceDir}`);
|
|
123
|
+
}
|
|
124
|
+
mkdirSync(dirname(destTarball), { recursive: true });
|
|
125
|
+
await tar.create({ gzip: true, file: destTarball, cwd: sourceDir }, ["."]);
|
|
126
|
+
}
|
|
127
|
+
async function untarDirectory(tarballPath, destDir) {
|
|
128
|
+
if (!existsSync2(tarballPath)) {
|
|
129
|
+
throw new Error(`[gcsTransport] tarball missing: ${tarballPath}`);
|
|
130
|
+
}
|
|
131
|
+
if (existsSync2(destDir)) {
|
|
132
|
+
rmSync(destDir, { recursive: true, force: true });
|
|
133
|
+
}
|
|
134
|
+
mkdirSync(destDir, { recursive: true });
|
|
135
|
+
await tar.extract({ file: tarballPath, cwd: destDir });
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// src/server.ts
|
|
139
|
+
var cachedStorage = null;
|
|
140
|
+
function getStorage() {
|
|
141
|
+
if (cachedStorage) return cachedStorage;
|
|
142
|
+
cachedStorage = new Storage();
|
|
143
|
+
return cachedStorage;
|
|
144
|
+
}
|
|
145
|
+
async function dispatch(event, deps) {
|
|
146
|
+
const unwrapped = unwrapEvent(event);
|
|
147
|
+
validateEventGcsUris(unwrapped);
|
|
148
|
+
logEvent({ event: "handler_start", action: unwrapped.Action, input: summarizeEvent(unwrapped) });
|
|
149
|
+
try {
|
|
150
|
+
switch (unwrapped.Action) {
|
|
151
|
+
case "plan":
|
|
152
|
+
return await handlePlan(unwrapped, deps);
|
|
153
|
+
case "renderChunk":
|
|
154
|
+
return await handleRenderChunk(unwrapped, deps);
|
|
155
|
+
case "assemble":
|
|
156
|
+
return await handleAssemble(unwrapped, deps);
|
|
157
|
+
default: {
|
|
158
|
+
const _exhaustive = unwrapped;
|
|
159
|
+
throw new Error(
|
|
160
|
+
`[handler] unknown Action: ${JSON.stringify(
|
|
161
|
+
_exhaustive.Action
|
|
162
|
+
)}. Expected one of "plan", "renderChunk", "assemble".`
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
} catch (err) {
|
|
167
|
+
logEvent({
|
|
168
|
+
event: "handler_error",
|
|
169
|
+
action: unwrapped.Action,
|
|
170
|
+
message: err instanceof Error ? err.message : String(err),
|
|
171
|
+
name: err instanceof Error ? err.name : void 0
|
|
172
|
+
});
|
|
173
|
+
throw err;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
var MAX_ENVELOPE_DEPTH = 4;
|
|
177
|
+
function unwrapEvent(event) {
|
|
178
|
+
let cursor = event;
|
|
179
|
+
for (let i = 0; i < MAX_ENVELOPE_DEPTH; i++) {
|
|
180
|
+
if (cursor && typeof cursor === "object") {
|
|
181
|
+
const obj = cursor;
|
|
182
|
+
if (typeof obj.Action === "string" && isCloudRunAction(obj.Action)) {
|
|
183
|
+
return cursor;
|
|
184
|
+
}
|
|
185
|
+
if ("Payload" in obj) {
|
|
186
|
+
cursor = obj.Payload;
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
if ("Input" in obj) {
|
|
190
|
+
cursor = obj.Input;
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
throw new Error(
|
|
197
|
+
`[handler] body has no recognised Action; unwrapped ${MAX_ENVELOPE_DEPTH} levels of Payload/Input without finding one.`
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
function isCloudRunAction(value) {
|
|
201
|
+
return value === "plan" || value === "renderChunk" || value === "assemble";
|
|
202
|
+
}
|
|
203
|
+
function logEvent(payload) {
|
|
204
|
+
console.log(JSON.stringify(payload));
|
|
205
|
+
}
|
|
206
|
+
function summarizeEvent(event) {
|
|
207
|
+
switch (event.Action) {
|
|
208
|
+
case "plan":
|
|
209
|
+
return {
|
|
210
|
+
projectGcsUri: event.ProjectGcsUri,
|
|
211
|
+
planOutputGcsPrefix: event.PlanOutputGcsPrefix,
|
|
212
|
+
format: event.Config.format,
|
|
213
|
+
fps: event.Config.fps
|
|
214
|
+
};
|
|
215
|
+
case "renderChunk":
|
|
216
|
+
return {
|
|
217
|
+
planGcsUri: event.PlanGcsUri,
|
|
218
|
+
chunkIndex: event.ChunkIndex,
|
|
219
|
+
format: event.Format
|
|
220
|
+
};
|
|
221
|
+
case "assemble":
|
|
222
|
+
return {
|
|
223
|
+
planGcsUri: event.PlanGcsUri,
|
|
224
|
+
chunkCount: event.ChunkGcsUris.length,
|
|
225
|
+
hasAudio: event.AudioGcsUri !== null,
|
|
226
|
+
outputGcsUri: event.OutputGcsUri,
|
|
227
|
+
format: event.Format
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
function primeChrome(deps) {
|
|
232
|
+
if (deps?.skipChromeResolution) return;
|
|
233
|
+
if (process.env.PRODUCER_HEADLESS_SHELL_PATH) return;
|
|
234
|
+
process.env.PRODUCER_HEADLESS_SHELL_PATH = resolveChromeExecutablePath();
|
|
235
|
+
}
|
|
236
|
+
async function handlePlan(event, deps) {
|
|
237
|
+
const started = Date.now();
|
|
238
|
+
const storage = deps?.storage ?? getStorage();
|
|
239
|
+
const primitive = deps?.primitives?.plan ?? plan;
|
|
240
|
+
primeChrome(deps);
|
|
241
|
+
const work = mkdtempSync(join(deps?.tmpRoot ?? tmpdir(), "hf-cr-plan-"));
|
|
242
|
+
const projectArchive = join(work, "project.tar.gz");
|
|
243
|
+
const projectDir = join(work, "project");
|
|
244
|
+
const planDir = join(work, "plan");
|
|
245
|
+
try {
|
|
246
|
+
await downloadGcsObjectToFile(storage, event.ProjectGcsUri, projectArchive);
|
|
247
|
+
await untarDirectory(projectArchive, projectDir);
|
|
248
|
+
const config = {
|
|
249
|
+
...event.Config
|
|
250
|
+
};
|
|
251
|
+
const result = await primitive(projectDir, config, planDir);
|
|
252
|
+
const planTar = join(work, "plan.tar.gz");
|
|
253
|
+
await tarDirectory(planDir, planTar);
|
|
254
|
+
const planTarUri = `${trimTrailingSlash(event.PlanOutputGcsPrefix)}/plan.tar.gz`;
|
|
255
|
+
const audioPath = join(planDir, "audio.aac");
|
|
256
|
+
const hasAudio = existsSync3(audioPath) && statSync2(audioPath).size > 0;
|
|
257
|
+
await uploadFileToGcs(storage, planTar, planTarUri, "application/gzip");
|
|
258
|
+
return {
|
|
259
|
+
Action: "plan",
|
|
260
|
+
PlanGcsUri: planTarUri,
|
|
261
|
+
PlanHash: result.planHash,
|
|
262
|
+
ChunkCount: result.chunkCount,
|
|
263
|
+
TotalFrames: result.totalFrames,
|
|
264
|
+
Fps: result.fps,
|
|
265
|
+
Width: result.width,
|
|
266
|
+
Height: result.height,
|
|
267
|
+
Format: result.format,
|
|
268
|
+
HasAudio: hasAudio,
|
|
269
|
+
AudioGcsUri: null,
|
|
270
|
+
FfmpegVersion: result.ffmpegVersion,
|
|
271
|
+
ProducerVersion: result.producerVersion,
|
|
272
|
+
DurationMs: Date.now() - started
|
|
273
|
+
};
|
|
274
|
+
} finally {
|
|
275
|
+
cleanupDir(work);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
async function handleRenderChunk(event, deps) {
|
|
279
|
+
const started = Date.now();
|
|
280
|
+
const storage = deps?.storage ?? getStorage();
|
|
281
|
+
const primitive = deps?.primitives?.renderChunk ?? renderChunk;
|
|
282
|
+
primeChrome(deps);
|
|
283
|
+
const work = mkdtempSync(join(deps?.tmpRoot ?? tmpdir(), "hf-cr-chunk-"));
|
|
284
|
+
const planTar = join(work, "plan.tar.gz");
|
|
285
|
+
const planDir = join(work, "plan");
|
|
286
|
+
try {
|
|
287
|
+
await downloadGcsObjectToFile(storage, event.PlanGcsUri, planTar);
|
|
288
|
+
await untarDirectory(planTar, planDir);
|
|
289
|
+
verifyPlanHash(planDir, event.PlanHash);
|
|
290
|
+
const chunkOutputBase = join(
|
|
291
|
+
work,
|
|
292
|
+
event.Format === "png-sequence" ? `chunk-${pad(event.ChunkIndex)}` : `chunk-${pad(event.ChunkIndex)}${formatExtension(event.Format)}`
|
|
293
|
+
);
|
|
294
|
+
const result = await primitive(planDir, event.ChunkIndex, chunkOutputBase);
|
|
295
|
+
const chunkUri = await uploadChunkOutput(
|
|
296
|
+
storage,
|
|
297
|
+
result,
|
|
298
|
+
event.ChunkOutputGcsPrefix,
|
|
299
|
+
event.ChunkIndex
|
|
300
|
+
);
|
|
301
|
+
return {
|
|
302
|
+
Action: "renderChunk",
|
|
303
|
+
ChunkGcsUri: chunkUri,
|
|
304
|
+
ChunkIndex: event.ChunkIndex,
|
|
305
|
+
Sha256: result.sha256,
|
|
306
|
+
FramesEncoded: result.framesEncoded,
|
|
307
|
+
DurationMs: Date.now() - started
|
|
308
|
+
};
|
|
309
|
+
} finally {
|
|
310
|
+
cleanupDir(work);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
async function uploadChunkOutput(storage, result, prefix, chunkIndex) {
|
|
314
|
+
const trimmed = trimTrailingSlash(prefix);
|
|
315
|
+
if (result.outputKind === "file") {
|
|
316
|
+
const ext = extname(result.outputPath);
|
|
317
|
+
const uri2 = `${trimmed}/chunks/${pad(chunkIndex)}${ext}`;
|
|
318
|
+
await uploadFileToGcs(storage, result.outputPath, uri2);
|
|
319
|
+
return uri2;
|
|
320
|
+
}
|
|
321
|
+
const tarball = `${result.outputPath}.tar.gz`;
|
|
322
|
+
await tarDirectory(result.outputPath, tarball);
|
|
323
|
+
const uri = `${trimmed}/chunks/${pad(chunkIndex)}.tar.gz`;
|
|
324
|
+
await uploadFileToGcs(storage, tarball, uri, "application/gzip");
|
|
325
|
+
return uri;
|
|
326
|
+
}
|
|
327
|
+
async function handleAssemble(event, deps) {
|
|
328
|
+
const started = Date.now();
|
|
329
|
+
const storage = deps?.storage ?? getStorage();
|
|
330
|
+
const primitive = deps?.primitives?.assemble ?? assemble;
|
|
331
|
+
const work = mkdtempSync(join(deps?.tmpRoot ?? tmpdir(), "hf-cr-assemble-"));
|
|
332
|
+
const planTar = join(work, "plan.tar.gz");
|
|
333
|
+
const planDir = join(work, "plan");
|
|
334
|
+
try {
|
|
335
|
+
await downloadGcsObjectToFile(storage, event.PlanGcsUri, planTar);
|
|
336
|
+
await untarDirectory(planTar, planDir);
|
|
337
|
+
const chunkPaths = await downloadChunkObjects(storage, event.ChunkGcsUris, work, event.Format);
|
|
338
|
+
let audioPath = null;
|
|
339
|
+
const planAudio = join(planDir, "audio.aac");
|
|
340
|
+
if (existsSync3(planAudio) && statSync2(planAudio).size > 0) {
|
|
341
|
+
audioPath = planAudio;
|
|
342
|
+
} else if (event.AudioGcsUri) {
|
|
343
|
+
audioPath = planAudio;
|
|
344
|
+
await downloadGcsObjectToFile(storage, event.AudioGcsUri, audioPath);
|
|
345
|
+
}
|
|
346
|
+
const finalOutput = event.Format === "png-sequence" ? join(work, "output-frames") : join(work, `output${formatExtension(event.Format)}`);
|
|
347
|
+
const result = await primitive(planDir, chunkPaths, audioPath, finalOutput, {
|
|
348
|
+
cfr: event.Cfr === true
|
|
349
|
+
});
|
|
350
|
+
if (event.Format === "png-sequence") {
|
|
351
|
+
const tarball = `${finalOutput}.tar.gz`;
|
|
352
|
+
await tarDirectory(finalOutput, tarball);
|
|
353
|
+
await uploadFileToGcs(storage, tarball, event.OutputGcsUri, "application/gzip");
|
|
354
|
+
} else {
|
|
355
|
+
await uploadFileToGcs(storage, finalOutput, event.OutputGcsUri);
|
|
356
|
+
}
|
|
357
|
+
return {
|
|
358
|
+
Action: "assemble",
|
|
359
|
+
OutputGcsUri: event.OutputGcsUri,
|
|
360
|
+
FramesEncoded: result.framesEncoded,
|
|
361
|
+
FileSize: result.fileSize,
|
|
362
|
+
DurationMs: Date.now() - started
|
|
363
|
+
};
|
|
364
|
+
} finally {
|
|
365
|
+
cleanupDir(work);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
async function downloadChunkObjects(storage, uris, workDir, format) {
|
|
369
|
+
const chunksDir = join(workDir, "chunks");
|
|
370
|
+
mkdirSync2(chunksDir, { recursive: true });
|
|
371
|
+
const local = new Array(uris.length);
|
|
372
|
+
await Promise.all(
|
|
373
|
+
uris.map(async (uri, i) => {
|
|
374
|
+
if (!uri) {
|
|
375
|
+
throw new Error(`[handler] chunk URI at index ${i} is empty`);
|
|
376
|
+
}
|
|
377
|
+
const { key } = parseGcsUri(uri);
|
|
378
|
+
const localPath = join(chunksDir, basename(key));
|
|
379
|
+
await downloadGcsObjectToFile(storage, uri, localPath);
|
|
380
|
+
if (format === "png-sequence") {
|
|
381
|
+
const dirPath = join(chunksDir, `frames-${pad(i)}`);
|
|
382
|
+
await untarDirectory(localPath, dirPath);
|
|
383
|
+
local[i] = dirPath;
|
|
384
|
+
} else {
|
|
385
|
+
local[i] = localPath;
|
|
386
|
+
}
|
|
387
|
+
})
|
|
388
|
+
);
|
|
389
|
+
return local;
|
|
390
|
+
}
|
|
391
|
+
function getEventGcsUris(event) {
|
|
392
|
+
switch (event.Action) {
|
|
393
|
+
case "plan":
|
|
394
|
+
return [event.ProjectGcsUri, event.PlanOutputGcsPrefix];
|
|
395
|
+
case "renderChunk":
|
|
396
|
+
return [event.PlanGcsUri, event.ChunkOutputGcsPrefix];
|
|
397
|
+
case "assemble":
|
|
398
|
+
return [
|
|
399
|
+
event.PlanGcsUri,
|
|
400
|
+
...event.ChunkGcsUris,
|
|
401
|
+
event.OutputGcsUri,
|
|
402
|
+
event.AudioGcsUri
|
|
403
|
+
].filter((u) => u != null);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
var warnedAllowlistDisabled = false;
|
|
407
|
+
function validateEventGcsUris(event) {
|
|
408
|
+
const allowedBucket = process.env.HYPERFRAMES_RENDER_BUCKET?.trim();
|
|
409
|
+
if (allowedBucket === "*") return;
|
|
410
|
+
if (!allowedBucket) {
|
|
411
|
+
if (!warnedAllowlistDisabled) {
|
|
412
|
+
warnedAllowlistDisabled = true;
|
|
413
|
+
logEvent({
|
|
414
|
+
event: "bucket_allowlist_disabled",
|
|
415
|
+
level: "WARNING",
|
|
416
|
+
message: 'HYPERFRAMES_RENDER_BUCKET is unset \u2014 the GCS bucket-allowlist guard is DISABLED. Set it to the render bucket name to enforce, or to "*" to opt out intentionally.'
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
for (const uri of getEventGcsUris(event)) {
|
|
422
|
+
const { bucket } = parseGcsUri(uri);
|
|
423
|
+
if (bucket !== allowedBucket) {
|
|
424
|
+
const err = new Error(
|
|
425
|
+
`[handler] GCS_URI_NOT_ALLOWED: URI ${JSON.stringify(uri)} targets bucket "${bucket}" but only "${allowedBucket}" is permitted`
|
|
426
|
+
);
|
|
427
|
+
err.name = "GCS_URI_NOT_ALLOWED";
|
|
428
|
+
throw err;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
function pad(n) {
|
|
433
|
+
return n.toString().padStart(4, "0");
|
|
434
|
+
}
|
|
435
|
+
function trimTrailingSlash(prefix) {
|
|
436
|
+
return prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
|
|
437
|
+
}
|
|
438
|
+
function cleanupDir(dir) {
|
|
439
|
+
try {
|
|
440
|
+
rmSync2(dir, { recursive: true, force: true });
|
|
441
|
+
} catch {
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
function verifyPlanHash(planDir, expected) {
|
|
445
|
+
const planJsonPath = join(planDir, "plan.json");
|
|
446
|
+
let parsed;
|
|
447
|
+
try {
|
|
448
|
+
parsed = JSON.parse(readFileSync(planJsonPath, "utf-8"));
|
|
449
|
+
} catch (err) {
|
|
450
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
451
|
+
const error = new Error(`PLAN_HASH_MISMATCH: failed to read ${planJsonPath}: ${msg}`);
|
|
452
|
+
error.name = "PLAN_HASH_MISMATCH";
|
|
453
|
+
throw error;
|
|
454
|
+
}
|
|
455
|
+
const actual = parsed.planHash;
|
|
456
|
+
if (typeof actual !== "string" || actual !== expected) {
|
|
457
|
+
const error = new Error(
|
|
458
|
+
`PLAN_HASH_MISMATCH: event PlanHash=${expected} did not match plan.json planHash=${String(actual)}`
|
|
459
|
+
);
|
|
460
|
+
error.name = "PLAN_HASH_MISMATCH";
|
|
461
|
+
throw error;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
var NON_RETRYABLE_ERROR_NAMES = /* @__PURE__ */ new Set([
|
|
465
|
+
// Handler-boundary guards.
|
|
466
|
+
"GCS_URI_NOT_ALLOWED",
|
|
467
|
+
"PLAN_HASH_MISMATCH",
|
|
468
|
+
// Producer error class names (`.name`) + their string code aliases — the
|
|
469
|
+
// class sets `.name` to the class name but wraps a `code`; cover both so a
|
|
470
|
+
// raw-code throw is caught too. Mirrors the AWS state machine's
|
|
471
|
+
// non-retryable list.
|
|
472
|
+
"FormatNotSupportedInDistributedError",
|
|
473
|
+
"PlanTooLargeError",
|
|
474
|
+
"RenderChunkValidationError",
|
|
475
|
+
"FFMPEG_VERSION_MISMATCH",
|
|
476
|
+
"FORMAT_NOT_SUPPORTED_IN_DISTRIBUTED",
|
|
477
|
+
"PLAN_TOO_LARGE",
|
|
478
|
+
"BROWSER_GPU_NOT_SOFTWARE",
|
|
479
|
+
"FONT_FETCH_FAILED",
|
|
480
|
+
"ChromeBinaryUnavailableError"
|
|
481
|
+
]);
|
|
482
|
+
function createApp(deps) {
|
|
483
|
+
const app = new Hono();
|
|
484
|
+
app.get("/healthz", (c) => c.json({ status: "ok" }));
|
|
485
|
+
app.post("/", async (c) => {
|
|
486
|
+
let body;
|
|
487
|
+
try {
|
|
488
|
+
body = await c.req.json();
|
|
489
|
+
} catch {
|
|
490
|
+
return c.json({ error: "BAD_REQUEST", message: "request body must be JSON" }, 400);
|
|
491
|
+
}
|
|
492
|
+
try {
|
|
493
|
+
const result = await dispatch(body, deps);
|
|
494
|
+
return c.json(result, 200);
|
|
495
|
+
} catch (err) {
|
|
496
|
+
const name = err instanceof Error ? err.name : void 0;
|
|
497
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
498
|
+
const status = name && NON_RETRYABLE_ERROR_NAMES.has(name) ? 400 : 500;
|
|
499
|
+
return c.json({ error: name ?? "RenderError", message }, status);
|
|
500
|
+
}
|
|
501
|
+
});
|
|
502
|
+
return app;
|
|
503
|
+
}
|
|
504
|
+
function startServer() {
|
|
505
|
+
const port = Number(process.env.PORT ?? 8080);
|
|
506
|
+
const app = createApp();
|
|
507
|
+
serve({ fetch: app.fetch, port }, (info) => {
|
|
508
|
+
logEvent({ event: "server_listening", port: info.port });
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
|
|
512
|
+
startServer();
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// src/sdk/deploySite.ts
|
|
516
|
+
import { mkdtempSync as mkdtempSync2, rmSync as rmSync3, statSync as statSync3 } from "node:fs";
|
|
517
|
+
import { tmpdir as tmpdir2 } from "node:os";
|
|
518
|
+
import { join as join2 } from "node:path";
|
|
519
|
+
import { Storage as Storage2 } from "@google-cloud/storage";
|
|
520
|
+
import { hashProjectDir } from "@hyperframes/producer/distributed";
|
|
521
|
+
async function deploySite(opts) {
|
|
522
|
+
if (!statSync3(opts.projectDir).isDirectory()) {
|
|
523
|
+
throw new Error(`[deploySite] projectDir is not a directory: ${opts.projectDir}`);
|
|
524
|
+
}
|
|
525
|
+
const siteId = opts.siteId ?? hashProjectDir(opts.projectDir);
|
|
526
|
+
const key = `sites/${siteId}/project.tar.gz`;
|
|
527
|
+
const projectGcsUri = formatGcsUri({ bucket: opts.bucketName, key });
|
|
528
|
+
const storage = opts.storage ?? new Storage2();
|
|
529
|
+
const file = storage.bucket(opts.bucketName).file(key);
|
|
530
|
+
const existing = await headObject(file);
|
|
531
|
+
if (existing) {
|
|
532
|
+
return {
|
|
533
|
+
siteId,
|
|
534
|
+
bucketName: opts.bucketName,
|
|
535
|
+
projectGcsUri,
|
|
536
|
+
bytes: existing.bytes,
|
|
537
|
+
uploadedAt: existing.lastModified,
|
|
538
|
+
uploaded: false
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
const workdir = mkdtempSync2(join2(tmpdir2(), "hf-deploy-site-"));
|
|
542
|
+
try {
|
|
543
|
+
const tarball = join2(workdir, "project.tar.gz");
|
|
544
|
+
await tarDirectory(opts.projectDir, tarball);
|
|
545
|
+
const size = statSync3(tarball).size;
|
|
546
|
+
await uploadFileToGcs(storage, tarball, projectGcsUri, "application/gzip");
|
|
547
|
+
return {
|
|
548
|
+
siteId,
|
|
549
|
+
bucketName: opts.bucketName,
|
|
550
|
+
projectGcsUri,
|
|
551
|
+
bytes: size,
|
|
552
|
+
uploadedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
553
|
+
uploaded: true
|
|
554
|
+
};
|
|
555
|
+
} finally {
|
|
556
|
+
rmSync3(workdir, { recursive: true, force: true });
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
async function headObject(file) {
|
|
560
|
+
const [exists] = await file.exists();
|
|
561
|
+
if (!exists) return null;
|
|
562
|
+
const [meta] = await file.getMetadata();
|
|
563
|
+
const sizeRaw = meta.size;
|
|
564
|
+
const bytes = typeof sizeRaw === "string" ? Number(sizeRaw) : typeof sizeRaw === "number" ? sizeRaw : 0;
|
|
565
|
+
return {
|
|
566
|
+
bytes: Number.isFinite(bytes) ? bytes : 0,
|
|
567
|
+
lastModified: meta.updated ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// src/sdk/renderToCloudRun.ts
|
|
572
|
+
import { randomUUID } from "node:crypto";
|
|
573
|
+
|
|
574
|
+
// src/sdk/validateConfig.ts
|
|
575
|
+
import { InvalidConfigError } from "@hyperframes/producer/distributed";
|
|
576
|
+
import {
|
|
577
|
+
InvalidConfigError as InvalidConfigError2,
|
|
578
|
+
validateDistributedRenderConfig,
|
|
579
|
+
validateVariablesPayload
|
|
580
|
+
} from "@hyperframes/producer/distributed";
|
|
581
|
+
var MAX_WORKFLOWS_INPUT_BYTES = 512 * 1024;
|
|
582
|
+
var LARGE_VARIABLES_DOCS_URL = "https://hyperframes.heygen.com/deploy/templates-on-lambda#working-with-large-variables";
|
|
583
|
+
function validateWorkflowsInputSize(input) {
|
|
584
|
+
let serialized;
|
|
585
|
+
try {
|
|
586
|
+
serialized = JSON.stringify(input);
|
|
587
|
+
} catch (err) {
|
|
588
|
+
throw new InvalidConfigError(
|
|
589
|
+
"config",
|
|
590
|
+
`Cloud Workflows execution argument is not JSON-serializable: ${err instanceof Error ? err.message : String(err)}`
|
|
591
|
+
);
|
|
592
|
+
}
|
|
593
|
+
if (serialized === void 0) {
|
|
594
|
+
throw new InvalidConfigError(
|
|
595
|
+
"config",
|
|
596
|
+
"Cloud Workflows execution argument is not JSON-serializable (JSON.stringify returned undefined). Check that all fields, including config.variables, are plain JSON values."
|
|
597
|
+
);
|
|
598
|
+
}
|
|
599
|
+
const byteLength = Buffer.byteLength(serialized, "utf8");
|
|
600
|
+
if (byteLength > MAX_WORKFLOWS_INPUT_BYTES) {
|
|
601
|
+
throw new InvalidConfigError(
|
|
602
|
+
"config",
|
|
603
|
+
`Cloud Workflows execution argument is ${byteLength} bytes, which exceeds the ${MAX_WORKFLOWS_INPUT_BYTES}-byte (512 KiB) limit. Variables are for typed data (strings, numbers, structured records); media assets (images, audio, video) should be passed as URL references the composition resolves at render time, not inlined as base64. See ${LARGE_VARIABLES_DOCS_URL} for the URL-your-assets convention.`
|
|
604
|
+
);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// src/sdk/renderToCloudRun.ts
|
|
609
|
+
async function renderToCloudRun(opts) {
|
|
610
|
+
validateDistributedRenderConfig(opts.config);
|
|
611
|
+
if (!opts.bucketName) throw new Error("[renderToCloudRun] bucketName is required");
|
|
612
|
+
if (!opts.projectId) throw new Error("[renderToCloudRun] projectId is required");
|
|
613
|
+
if (!opts.location) throw new Error("[renderToCloudRun] location is required");
|
|
614
|
+
if (!opts.workflowId) throw new Error("[renderToCloudRun] workflowId is required");
|
|
615
|
+
if (!opts.serviceUrl) throw new Error("[renderToCloudRun] serviceUrl is required");
|
|
616
|
+
if (!opts.siteHandle && !opts.projectDir) {
|
|
617
|
+
throw new Error("[renderToCloudRun] either siteHandle or projectDir must be supplied");
|
|
618
|
+
}
|
|
619
|
+
const renderId = opts.renderId ?? `hf-render-${randomUUID()}`;
|
|
620
|
+
if (!/^[A-Za-z0-9._-]+$/.test(renderId) || renderId.includes("..")) {
|
|
621
|
+
throw new Error(
|
|
622
|
+
`[renderToCloudRun] renderId must match [A-Za-z0-9._-]+ and not contain "..": ${JSON.stringify(renderId)}`
|
|
623
|
+
);
|
|
624
|
+
}
|
|
625
|
+
const ext = formatExtension(opts.config.format);
|
|
626
|
+
const outputKey = opts.outputKey ?? `renders/${renderId}/output${ext}`;
|
|
627
|
+
const planOutputGcsPrefix = formatGcsUri({
|
|
628
|
+
bucket: opts.bucketName,
|
|
629
|
+
key: `renders/${renderId}/`
|
|
630
|
+
});
|
|
631
|
+
const outputGcsUri = formatGcsUri({ bucket: opts.bucketName, key: outputKey });
|
|
632
|
+
const site = opts.siteHandle ?? await deploySite({
|
|
633
|
+
projectDir: opts.projectDir,
|
|
634
|
+
bucketName: opts.bucketName,
|
|
635
|
+
storage: opts.storage
|
|
636
|
+
});
|
|
637
|
+
const argument = {
|
|
638
|
+
RenderId: renderId,
|
|
639
|
+
ProjectGcsUri: site.projectGcsUri,
|
|
640
|
+
PlanOutputGcsPrefix: planOutputGcsPrefix,
|
|
641
|
+
OutputGcsUri: outputGcsUri,
|
|
642
|
+
ServiceUrl: opts.serviceUrl,
|
|
643
|
+
Config: opts.config
|
|
644
|
+
};
|
|
645
|
+
validateWorkflowsInputSize(argument);
|
|
646
|
+
const executions = opts.executions ?? await defaultExecutionsClient();
|
|
647
|
+
const parent = executions.workflowPath(opts.projectId, opts.location, opts.workflowId);
|
|
648
|
+
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
649
|
+
const [execution] = await executions.createExecution({
|
|
650
|
+
parent,
|
|
651
|
+
execution: { argument: JSON.stringify(argument) }
|
|
652
|
+
});
|
|
653
|
+
if (!execution.name) {
|
|
654
|
+
throw new Error("[renderToCloudRun] CreateExecution returned no execution name");
|
|
655
|
+
}
|
|
656
|
+
return {
|
|
657
|
+
renderId,
|
|
658
|
+
executionName: execution.name,
|
|
659
|
+
bucketName: opts.bucketName,
|
|
660
|
+
workflowId: opts.workflowId,
|
|
661
|
+
outputGcsUri,
|
|
662
|
+
projectGcsUri: site.projectGcsUri,
|
|
663
|
+
startedAt
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
async function defaultExecutionsClient() {
|
|
667
|
+
const mod = await import("@google-cloud/workflows");
|
|
668
|
+
const client = new mod.ExecutionsClient();
|
|
669
|
+
return client;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// src/sdk/costAccounting.ts
|
|
673
|
+
var CLOUD_RUN_USD_PER_VCPU_SECOND = 24e-6;
|
|
674
|
+
var CLOUD_RUN_USD_PER_GIB_SECOND = 25e-7;
|
|
675
|
+
var CLOUD_RUN_USD_PER_REQUEST = 4e-7;
|
|
676
|
+
var WORKFLOWS_USD_PER_STEP = 1e-5;
|
|
677
|
+
function computeRenderCost(invocations, workflowSteps) {
|
|
678
|
+
let cloudRunUsd = 0;
|
|
679
|
+
let anyEstimated = false;
|
|
680
|
+
for (const inv of invocations) {
|
|
681
|
+
const seconds = inv.durationMs / 1e3;
|
|
682
|
+
cloudRunUsd += seconds * inv.vcpu * CLOUD_RUN_USD_PER_VCPU_SECOND;
|
|
683
|
+
cloudRunUsd += seconds * inv.memoryGib * CLOUD_RUN_USD_PER_GIB_SECOND;
|
|
684
|
+
cloudRunUsd += CLOUD_RUN_USD_PER_REQUEST;
|
|
685
|
+
if (inv.estimated) anyEstimated = true;
|
|
686
|
+
}
|
|
687
|
+
const workflowsUsd = workflowSteps * WORKFLOWS_USD_PER_STEP;
|
|
688
|
+
const accruedSoFarUsd = roundUsd(cloudRunUsd + workflowsUsd);
|
|
689
|
+
return {
|
|
690
|
+
accruedSoFarUsd,
|
|
691
|
+
displayCost: formatUsd(accruedSoFarUsd),
|
|
692
|
+
breakdown: {
|
|
693
|
+
cloudRunUsd: roundUsd(cloudRunUsd),
|
|
694
|
+
workflowsUsd: roundUsd(workflowsUsd),
|
|
695
|
+
gcsEstimate: "not-included",
|
|
696
|
+
estimated: anyEstimated
|
|
697
|
+
}
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
function roundUsd(usd) {
|
|
701
|
+
return Math.round(usd * 1e4) / 1e4;
|
|
702
|
+
}
|
|
703
|
+
function formatUsd(usd) {
|
|
704
|
+
return `$${usd.toFixed(4)}`;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// src/sdk/getRenderProgress.ts
|
|
708
|
+
var DEFAULT_VCPU = 4;
|
|
709
|
+
var DEFAULT_MEMORY_GIB = 16;
|
|
710
|
+
async function getRenderProgress(opts) {
|
|
711
|
+
if (!opts.executionName) {
|
|
712
|
+
throw new Error("[getRenderProgress] executionName is required");
|
|
713
|
+
}
|
|
714
|
+
const executions = opts.executions ?? await defaultExecutionsClient2();
|
|
715
|
+
const vcpu = opts.vcpu ?? DEFAULT_VCPU;
|
|
716
|
+
const memoryGib = opts.memoryGib ?? DEFAULT_MEMORY_GIB;
|
|
717
|
+
const [execution] = await executions.getExecution({ name: opts.executionName });
|
|
718
|
+
const status = mapState(execution.state);
|
|
719
|
+
const startedAt = toIso(execution.startTime) ?? (/* @__PURE__ */ new Date(0)).toISOString();
|
|
720
|
+
const endedAt = toIso(execution.endTime);
|
|
721
|
+
const errors = [];
|
|
722
|
+
if (execution.error) {
|
|
723
|
+
errors.push({
|
|
724
|
+
state: execution.error.context ?? "<execution>",
|
|
725
|
+
error: extractErrorName(execution.error.payload) ?? "ExecutionError",
|
|
726
|
+
cause: execution.error.payload ?? ""
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
if (status !== "succeeded") {
|
|
730
|
+
return {
|
|
731
|
+
status,
|
|
732
|
+
overallProgress: 0,
|
|
733
|
+
framesRendered: 0,
|
|
734
|
+
totalFrames: null,
|
|
735
|
+
invocationsObserved: 0,
|
|
736
|
+
costs: computeRenderCost([], 0),
|
|
737
|
+
outputFile: null,
|
|
738
|
+
errors,
|
|
739
|
+
fatalErrorEncountered: status === "failed" || status === "cancelled",
|
|
740
|
+
startedAt,
|
|
741
|
+
endedAt
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
const acc = parseAccumulated(execution.result);
|
|
745
|
+
const chunks = acc.Chunks?.filter((c) => c != null) ?? [];
|
|
746
|
+
const framesRendered = chunks.reduce((sum, c) => sum + (c.FramesEncoded ?? 0), 0);
|
|
747
|
+
const totalFrames = typeof acc.Plan?.TotalFrames === "number" ? acc.Plan.TotalFrames : null;
|
|
748
|
+
const invocations = [];
|
|
749
|
+
const pushInv = (durationMs) => {
|
|
750
|
+
invocations.push({
|
|
751
|
+
durationMs: typeof durationMs === "number" ? durationMs : 0,
|
|
752
|
+
vcpu,
|
|
753
|
+
memoryGib,
|
|
754
|
+
estimated: typeof durationMs !== "number"
|
|
755
|
+
});
|
|
756
|
+
};
|
|
757
|
+
if (acc.Plan) pushInv(acc.Plan.DurationMs);
|
|
758
|
+
for (const c of chunks) pushInv(c.DurationMs);
|
|
759
|
+
if (acc.Assemble) pushInv(acc.Assemble.DurationMs);
|
|
760
|
+
const workflowSteps = invocations.length + 4;
|
|
761
|
+
const costs = computeRenderCost(invocations, workflowSteps);
|
|
762
|
+
const outputGcsUri = acc.Assemble?.OutputGcsUri;
|
|
763
|
+
const outputFile = outputGcsUri ? {
|
|
764
|
+
gcsUri: outputGcsUri,
|
|
765
|
+
bytes: typeof acc.Assemble?.FileSize === "number" ? acc.Assemble.FileSize : null
|
|
766
|
+
} : null;
|
|
767
|
+
return {
|
|
768
|
+
status,
|
|
769
|
+
overallProgress: 1,
|
|
770
|
+
framesRendered,
|
|
771
|
+
totalFrames,
|
|
772
|
+
invocationsObserved: invocations.length,
|
|
773
|
+
costs,
|
|
774
|
+
outputFile,
|
|
775
|
+
errors,
|
|
776
|
+
fatalErrorEncountered: false,
|
|
777
|
+
startedAt,
|
|
778
|
+
endedAt
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
function mapState(state) {
|
|
782
|
+
switch (state) {
|
|
783
|
+
case "ACTIVE":
|
|
784
|
+
case "QUEUED":
|
|
785
|
+
return "running";
|
|
786
|
+
case "SUCCEEDED":
|
|
787
|
+
return "succeeded";
|
|
788
|
+
case "FAILED":
|
|
789
|
+
case "UNAVAILABLE":
|
|
790
|
+
return "failed";
|
|
791
|
+
case "CANCELLED":
|
|
792
|
+
return "cancelled";
|
|
793
|
+
default:
|
|
794
|
+
return "unknown";
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
function parseAccumulated(result) {
|
|
798
|
+
if (!result) return {};
|
|
799
|
+
try {
|
|
800
|
+
const parsed = JSON.parse(result);
|
|
801
|
+
if (parsed && typeof parsed === "object") return parsed;
|
|
802
|
+
} catch {
|
|
803
|
+
}
|
|
804
|
+
return {};
|
|
805
|
+
}
|
|
806
|
+
function extractErrorName(payload) {
|
|
807
|
+
if (!payload) return void 0;
|
|
808
|
+
try {
|
|
809
|
+
const outer = JSON.parse(payload);
|
|
810
|
+
if (typeof outer.error === "string") return outer.error;
|
|
811
|
+
if (typeof outer.body === "string") {
|
|
812
|
+
const inner = JSON.parse(outer.body);
|
|
813
|
+
if (typeof inner.error === "string") return inner.error;
|
|
814
|
+
} else if (outer.body && typeof outer.body === "object") {
|
|
815
|
+
const inner = outer.body;
|
|
816
|
+
if (typeof inner.error === "string") return inner.error;
|
|
817
|
+
}
|
|
818
|
+
} catch {
|
|
819
|
+
}
|
|
820
|
+
return void 0;
|
|
821
|
+
}
|
|
822
|
+
function toIso(ts) {
|
|
823
|
+
if (ts == null) return null;
|
|
824
|
+
if (typeof ts === "string") return ts;
|
|
825
|
+
const seconds = ts.seconds == null ? null : Number(ts.seconds);
|
|
826
|
+
if (seconds == null || !Number.isFinite(seconds)) return null;
|
|
827
|
+
const ms = seconds * 1e3 + (ts.nanos ?? 0) / 1e6;
|
|
828
|
+
return new Date(ms).toISOString();
|
|
829
|
+
}
|
|
830
|
+
async function defaultExecutionsClient2() {
|
|
831
|
+
const mod = await import("@google-cloud/workflows");
|
|
832
|
+
const client = new mod.ExecutionsClient();
|
|
833
|
+
return client;
|
|
834
|
+
}
|
|
835
|
+
export {
|
|
836
|
+
ChromeBinaryUnavailableError,
|
|
837
|
+
InvalidConfigError2 as InvalidConfigError,
|
|
838
|
+
computeRenderCost,
|
|
839
|
+
createApp,
|
|
840
|
+
deploySite,
|
|
841
|
+
dispatch,
|
|
842
|
+
downloadGcsObjectToFile,
|
|
843
|
+
formatGcsUri,
|
|
844
|
+
getRenderProgress,
|
|
845
|
+
parseGcsUri,
|
|
846
|
+
renderToCloudRun,
|
|
847
|
+
resolveChromeExecutablePath,
|
|
848
|
+
startServer,
|
|
849
|
+
tarDirectory,
|
|
850
|
+
untarDirectory,
|
|
851
|
+
unwrapEvent,
|
|
852
|
+
uploadFileToGcs,
|
|
853
|
+
validateDistributedRenderConfig
|
|
854
|
+
};
|
|
855
|
+
//# sourceMappingURL=index.js.map
|