@hyperframes/aws-lambda 0.8.177 → 0.8.178
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 +9 -4
- package/dist/_chunks/chunk-AK7J3QQN.js +1006 -0
- package/dist/_chunks/chunk-ON46F5DI.js +589 -0
- package/dist/_chunks/chunk-YS3KY3C5.js +19 -0
- package/dist/_chunks/chunk-ZY5YIH5W.js +380 -0
- package/dist/cdk/index.js +13 -1
- package/dist/contentStore.d.ts +5 -0
- package/dist/handler.js +5 -1137
- package/dist/index.js +26 -1648
- package/dist/s3PlanV2Publisher.d.ts +1 -0
- package/dist/s3Transport.d.ts +14 -1
- package/dist/sdk/deploySite.d.ts +17 -17
- package/dist/sdk/index.d.ts +1 -1
- package/dist/sdk/index.js +13 -573
- package/dist/sdk/renderToLambda.d.ts +2 -2
- package/dist/siteManifest.d.ts +18 -0
- package/package.json +3 -2
|
@@ -0,0 +1,589 @@
|
|
|
1
|
+
import {
|
|
2
|
+
SITE_MANIFEST_INVALID,
|
|
3
|
+
downloadS3ObjectToFile,
|
|
4
|
+
ensureContentStoreObject,
|
|
5
|
+
formatExtension,
|
|
6
|
+
formatS3Uri,
|
|
7
|
+
isS3NotFound,
|
|
8
|
+
isSiteManifestUri,
|
|
9
|
+
mapConcurrent,
|
|
10
|
+
parseS3Uri,
|
|
11
|
+
parseSiteManifest,
|
|
12
|
+
putContentStoreFile,
|
|
13
|
+
s3ObjectExists,
|
|
14
|
+
serializeSiteManifest,
|
|
15
|
+
sha256File,
|
|
16
|
+
siteManifestUri,
|
|
17
|
+
siteTarballUri,
|
|
18
|
+
uploadContentAddressedFileToS3
|
|
19
|
+
} from "./chunk-ZY5YIH5W.js";
|
|
20
|
+
import {
|
|
21
|
+
CONTENT_STORE_RETENTION_DAYS
|
|
22
|
+
} from "./chunk-YS3KY3C5.js";
|
|
23
|
+
|
|
24
|
+
// src/sdk/deploySite.ts
|
|
25
|
+
import { mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
26
|
+
import { tmpdir } from "node:os";
|
|
27
|
+
import { join } from "node:path";
|
|
28
|
+
import { S3Client } from "@aws-sdk/client-s3";
|
|
29
|
+
import { hashProjectDir, listSiteFiles } from "@hyperframes/producer/distributed";
|
|
30
|
+
function siteHandleFromId(bucketName, siteId) {
|
|
31
|
+
const projectS3Uri = siteManifestUri(bucketName, siteId);
|
|
32
|
+
return { siteId, bucketName, projectS3Uri, bytes: 0, uploadedAt: "", uploaded: false };
|
|
33
|
+
}
|
|
34
|
+
async function ensureSiteStored(site, s3, projectDir) {
|
|
35
|
+
if (!isSiteManifestUri(site.projectS3Uri)) return site;
|
|
36
|
+
const { bucket } = parseS3Uri(site.projectS3Uri);
|
|
37
|
+
const workdir = mkdtempSync(join(tmpdir(), "hf-site-check-"));
|
|
38
|
+
try {
|
|
39
|
+
const manifestPath = join(workdir, "files.json");
|
|
40
|
+
try {
|
|
41
|
+
await downloadS3ObjectToFile(s3, site.projectS3Uri, manifestPath);
|
|
42
|
+
} catch (error) {
|
|
43
|
+
if (!isS3NotFound(error)) throw error;
|
|
44
|
+
return await tarballSite(site, s3, bucket);
|
|
45
|
+
}
|
|
46
|
+
const files = parseSiteManifest(readFileSync(manifestPath, "utf-8"));
|
|
47
|
+
const missing = [];
|
|
48
|
+
await mapConcurrent(files, 16, async ({ path, sha256, sizeBytes }) => {
|
|
49
|
+
try {
|
|
50
|
+
await ensureContentStoreObject(s3, bucket, sha256, sizeBytes);
|
|
51
|
+
} catch (error) {
|
|
52
|
+
if (error.name !== "PLAN_ARTIFACT_MISSING") throw error;
|
|
53
|
+
if (!projectDir) return void missing.push(path);
|
|
54
|
+
await putContentStoreFile(s3, join(projectDir, path), bucket, sha256);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
if (missing.length > 0) {
|
|
58
|
+
const error = new Error(
|
|
59
|
+
`[deploySite] SITE_FILES_MISSING: ${missing.length} of ${files.length} files of site ${site.siteId} are no longer stored (unused for ${CONTENT_STORE_RETENTION_DAYS} days). Run \`hyperframes lambda sites create\`, or pass projectDir to re-upload them.`
|
|
60
|
+
);
|
|
61
|
+
error.name = "SITE_FILES_MISSING";
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
return site;
|
|
65
|
+
} finally {
|
|
66
|
+
rmSync(workdir, { recursive: true, force: true });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
async function tarballSite(site, s3, bucket) {
|
|
70
|
+
const projectS3Uri = siteTarballUri(bucket, site.siteId);
|
|
71
|
+
if (await s3ObjectExists(s3, projectS3Uri)) return { ...site, projectS3Uri };
|
|
72
|
+
const error = new Error(
|
|
73
|
+
`[deploySite] SITE_NOT_FOUND: site ${site.siteId} has neither ${site.projectS3Uri} nor ${projectS3Uri}. Run \`hyperframes lambda sites create\` again and use the new site id.`
|
|
74
|
+
);
|
|
75
|
+
error.name = "SITE_NOT_FOUND";
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
78
|
+
async function deploySite(opts) {
|
|
79
|
+
const s3 = opts.s3 ?? new S3Client({ region: opts.region });
|
|
80
|
+
if (!statSync(opts.projectDir).isDirectory()) {
|
|
81
|
+
throw new Error(`[deploySite] projectDir is not a directory: ${opts.projectDir}`);
|
|
82
|
+
}
|
|
83
|
+
const files = listSiteFiles(opts.projectDir);
|
|
84
|
+
const siteId = opts.siteId ?? hashProjectDir(opts.projectDir, files);
|
|
85
|
+
const projectS3Uri = siteManifestUri(opts.bucketName, siteId);
|
|
86
|
+
const stored = [];
|
|
87
|
+
let uploaded = false;
|
|
88
|
+
await mapConcurrent(files, 8, async (path) => {
|
|
89
|
+
const localPath = join(opts.projectDir, path);
|
|
90
|
+
const sha256 = await sha256File(localPath);
|
|
91
|
+
stored.push({ path, sha256, sizeBytes: statSync(localPath).size });
|
|
92
|
+
const outcome = await putContentStoreFile(s3, localPath, opts.bucketName, sha256);
|
|
93
|
+
uploaded ||= outcome === "uploaded";
|
|
94
|
+
});
|
|
95
|
+
stored.sort((a, b) => a.path < b.path ? -1 : 1);
|
|
96
|
+
const workdir = mkdtempSync(join(tmpdir(), "hf-deploy-site-"));
|
|
97
|
+
try {
|
|
98
|
+
const manifestPath = join(workdir, "files.json");
|
|
99
|
+
const manifest = serializeSiteManifest(stored);
|
|
100
|
+
writeFileSync(manifestPath, manifest);
|
|
101
|
+
const outcome = await uploadContentAddressedFileToS3(
|
|
102
|
+
s3,
|
|
103
|
+
manifestPath,
|
|
104
|
+
projectS3Uri,
|
|
105
|
+
await sha256File(manifestPath),
|
|
106
|
+
"application/json"
|
|
107
|
+
);
|
|
108
|
+
uploaded ||= outcome === "uploaded";
|
|
109
|
+
} finally {
|
|
110
|
+
rmSync(workdir, { recursive: true, force: true });
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
siteId,
|
|
114
|
+
bucketName: opts.bucketName,
|
|
115
|
+
projectS3Uri,
|
|
116
|
+
bytes: stored.reduce((sum, file) => sum + file.sizeBytes, 0),
|
|
117
|
+
uploadedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
118
|
+
uploaded
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// src/sdk/validateConfig.ts
|
|
123
|
+
import { InvalidConfigError } from "@hyperframes/producer/distributed";
|
|
124
|
+
import {
|
|
125
|
+
InvalidConfigError as InvalidConfigError2,
|
|
126
|
+
validateDistributedRenderConfig,
|
|
127
|
+
validateVariablesPayload
|
|
128
|
+
} from "@hyperframes/producer/distributed";
|
|
129
|
+
var MAX_STEP_FUNCTIONS_INPUT_BYTES = 256 * 1024;
|
|
130
|
+
var LARGE_VARIABLES_DOCS_URL = "https://hyperframes.heygen.com/deploy/templates-on-lambda#working-with-large-variables";
|
|
131
|
+
function validateStepFunctionsInputSize(input) {
|
|
132
|
+
let serialized;
|
|
133
|
+
try {
|
|
134
|
+
serialized = JSON.stringify(input);
|
|
135
|
+
} catch (err) {
|
|
136
|
+
throw new InvalidConfigError(
|
|
137
|
+
"config",
|
|
138
|
+
`Step Functions execution input is not JSON-serializable: ${err instanceof Error ? err.message : String(err)}`
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
if (serialized === void 0) {
|
|
142
|
+
throw new InvalidConfigError(
|
|
143
|
+
"config",
|
|
144
|
+
"Step Functions execution input is not JSON-serializable (JSON.stringify returned undefined). Check that all fields, including config.variables, are plain JSON values."
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
const byteLength = Buffer.byteLength(serialized, "utf8");
|
|
148
|
+
if (byteLength > MAX_STEP_FUNCTIONS_INPUT_BYTES) {
|
|
149
|
+
throw new InvalidConfigError(
|
|
150
|
+
"config",
|
|
151
|
+
`Step Functions execution input is ${byteLength} bytes, which exceeds the ${MAX_STEP_FUNCTIONS_INPUT_BYTES}-byte (256 KiB) limit for Standard workflows. 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.`
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// src/sdk/renderToLambda.ts
|
|
157
|
+
import { randomUUID } from "node:crypto";
|
|
158
|
+
import { DescribeStateMachineCommand, SFNClient, StartExecutionCommand } from "@aws-sdk/client-sfn";
|
|
159
|
+
import { S3Client as S3Client2 } from "@aws-sdk/client-s3";
|
|
160
|
+
async function renderToLambda(opts) {
|
|
161
|
+
validateDistributedRenderConfig(opts.config);
|
|
162
|
+
if (!opts.bucketName) {
|
|
163
|
+
throw new Error("[renderToLambda] bucketName is required");
|
|
164
|
+
}
|
|
165
|
+
if (!opts.stateMachineArn) {
|
|
166
|
+
throw new Error("[renderToLambda] stateMachineArn is required");
|
|
167
|
+
}
|
|
168
|
+
if (!opts.siteHandle && !opts.projectDir) {
|
|
169
|
+
throw new Error("[renderToLambda] either siteHandle or projectDir must be supplied");
|
|
170
|
+
}
|
|
171
|
+
const sfn = opts.sfn ?? new SFNClient({ region: opts.region });
|
|
172
|
+
await assertStackReadsSites(sfn, opts.stateMachineArn);
|
|
173
|
+
const executionName = opts.executionName ?? `hf-render-${randomUUID()}`;
|
|
174
|
+
const ext = formatExtension(opts.config.format);
|
|
175
|
+
const outputKey = opts.outputKey ?? `renders/${executionName}/output${ext}`;
|
|
176
|
+
const planOutputS3Prefix = formatS3Uri({
|
|
177
|
+
bucket: opts.bucketName,
|
|
178
|
+
key: `renders/${executionName}/`
|
|
179
|
+
});
|
|
180
|
+
const outputS3Uri = formatS3Uri({ bucket: opts.bucketName, key: outputKey });
|
|
181
|
+
const site = opts.siteHandle ? await ensureSiteStored(
|
|
182
|
+
opts.siteHandle,
|
|
183
|
+
opts.s3 ?? new S3Client2({ region: opts.region }),
|
|
184
|
+
opts.projectDir
|
|
185
|
+
) : await deploySite({
|
|
186
|
+
projectDir: opts.projectDir,
|
|
187
|
+
bucketName: opts.bucketName,
|
|
188
|
+
region: opts.region,
|
|
189
|
+
s3: opts.s3
|
|
190
|
+
});
|
|
191
|
+
const input = {
|
|
192
|
+
ProjectS3Uri: site.projectS3Uri,
|
|
193
|
+
PlanOutputS3Prefix: planOutputS3Prefix,
|
|
194
|
+
OutputS3Uri: outputS3Uri,
|
|
195
|
+
Config: opts.config,
|
|
196
|
+
PlanProtocol: opts.planProtocol ?? "v2"
|
|
197
|
+
};
|
|
198
|
+
validateStepFunctionsInputSize(input);
|
|
199
|
+
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
200
|
+
const response = await sfn.send(
|
|
201
|
+
new StartExecutionCommand({
|
|
202
|
+
stateMachineArn: opts.stateMachineArn,
|
|
203
|
+
name: executionName,
|
|
204
|
+
input: JSON.stringify(input)
|
|
205
|
+
})
|
|
206
|
+
);
|
|
207
|
+
if (!response.executionArn) {
|
|
208
|
+
throw new Error("[renderToLambda] StartExecution returned no executionArn");
|
|
209
|
+
}
|
|
210
|
+
return {
|
|
211
|
+
renderId: executionName,
|
|
212
|
+
executionArn: response.executionArn,
|
|
213
|
+
bucketName: opts.bucketName,
|
|
214
|
+
stateMachineArn: opts.stateMachineArn,
|
|
215
|
+
outputS3Uri,
|
|
216
|
+
projectS3Uri: site.projectS3Uri,
|
|
217
|
+
startedAt
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
async function assertStackReadsSites(sfn, stateMachineArn) {
|
|
221
|
+
const { definition } = await sfn.send(new DescribeStateMachineCommand({ stateMachineArn }));
|
|
222
|
+
if (definition?.includes(`"${SITE_MANIFEST_INVALID}"`)) return;
|
|
223
|
+
const error = new Error(
|
|
224
|
+
`[renderToLambda] STACK_TOO_OLD: ${stateMachineArn} was deployed by an earlier @hyperframes/aws-lambda that cannot read this version's sites. Redeploy the stack first (\`hyperframes lambda deploy\`, or the SAM template / CDK construct from this version).`
|
|
225
|
+
);
|
|
226
|
+
error.name = "STACK_TOO_OLD";
|
|
227
|
+
throw error;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// src/sdk/costAccounting.ts
|
|
231
|
+
var LAMBDA_USD_PER_GB_SECOND = 166667e-10;
|
|
232
|
+
var SFN_USD_PER_TRANSITION = 25e-6;
|
|
233
|
+
function computeRenderCost(lambdaInvocations, stateTransitions) {
|
|
234
|
+
let lambdaUsd = 0;
|
|
235
|
+
let anyEstimated = false;
|
|
236
|
+
for (const inv of lambdaInvocations) {
|
|
237
|
+
const gbSeconds = inv.memorySizeMb / 1024 * (inv.billedDurationMs / 1e3);
|
|
238
|
+
lambdaUsd += gbSeconds * LAMBDA_USD_PER_GB_SECOND;
|
|
239
|
+
if (inv.estimated) anyEstimated = true;
|
|
240
|
+
}
|
|
241
|
+
const stepFunctionsUsd = stateTransitions * SFN_USD_PER_TRANSITION;
|
|
242
|
+
const accruedSoFarUsd = roundUsd(lambdaUsd + stepFunctionsUsd);
|
|
243
|
+
return {
|
|
244
|
+
accruedSoFarUsd,
|
|
245
|
+
displayCost: formatUsd(accruedSoFarUsd),
|
|
246
|
+
breakdown: {
|
|
247
|
+
lambdaUsd: roundUsd(lambdaUsd),
|
|
248
|
+
stepFunctionsUsd: roundUsd(stepFunctionsUsd),
|
|
249
|
+
s3Estimate: "not-included",
|
|
250
|
+
estimated: anyEstimated
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
function roundUsd(usd) {
|
|
255
|
+
return Math.round(usd * 1e4) / 1e4;
|
|
256
|
+
}
|
|
257
|
+
function formatUsd(usd) {
|
|
258
|
+
return `$${usd.toFixed(4)}`;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// src/sdk/getRenderProgress.ts
|
|
262
|
+
import {
|
|
263
|
+
DescribeExecutionCommand,
|
|
264
|
+
DescribeMapRunCommand,
|
|
265
|
+
GetExecutionHistoryCommand,
|
|
266
|
+
ListExecutionsCommand,
|
|
267
|
+
SFNClient as SFNClient2
|
|
268
|
+
} from "@aws-sdk/client-sfn";
|
|
269
|
+
var DEFAULT_MEMORY_MB = 10240;
|
|
270
|
+
function createProgressSfnClient(config = {}) {
|
|
271
|
+
return new SFNClient2({ maxAttempts: 8, ...config });
|
|
272
|
+
}
|
|
273
|
+
async function getRenderProgress(opts) {
|
|
274
|
+
if (!opts.executionArn) {
|
|
275
|
+
throw new Error("[getRenderProgress] executionArn is required");
|
|
276
|
+
}
|
|
277
|
+
const sfn = opts.sfn ?? createProgressSfnClient({ region: opts.region });
|
|
278
|
+
const memoryMb = opts.defaultMemorySizeMb ?? DEFAULT_MEMORY_MB;
|
|
279
|
+
const describe = await sfn.send(
|
|
280
|
+
new DescribeExecutionCommand({ executionArn: opts.executionArn })
|
|
281
|
+
);
|
|
282
|
+
const status = describe.status ?? "RUNNING";
|
|
283
|
+
const startedAt = describe.startDate?.toISOString() ?? (/* @__PURE__ */ new Date(0)).toISOString();
|
|
284
|
+
const endedAt = describe.stopDate?.toISOString() ?? null;
|
|
285
|
+
const history = await loadFullHistory(sfn, opts.executionArn);
|
|
286
|
+
const summary = summarizeHistory(history, memoryMb);
|
|
287
|
+
const mapRun = findChunkMapRun(history);
|
|
288
|
+
const warnings = [];
|
|
289
|
+
if (mapRun?.exited) addChunkResults(summary, mapRun.output, memoryMb);
|
|
290
|
+
else if (mapRun) {
|
|
291
|
+
await addRunningMapRun(sfn, mapRun.arn, summary, memoryMb).catch((err) => {
|
|
292
|
+
if (err?.name !== "AccessDeniedException") throw err;
|
|
293
|
+
warnings.push(MAP_RUN_ACCESS_DENIED);
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
const costs = computeRenderCost(summary.lambdaInvocations, summary.stateTransitions);
|
|
297
|
+
const overallProgress = computeOverallProgress({
|
|
298
|
+
status,
|
|
299
|
+
totalFrames: summary.totalFrames,
|
|
300
|
+
framesRendered: summary.framesRendered,
|
|
301
|
+
assembleComplete: summary.assembleComplete
|
|
302
|
+
});
|
|
303
|
+
return {
|
|
304
|
+
status,
|
|
305
|
+
overallProgress,
|
|
306
|
+
framesRendered: summary.framesRendered,
|
|
307
|
+
totalFrames: summary.totalFrames,
|
|
308
|
+
lambdasInvoked: summary.lambdasInvoked,
|
|
309
|
+
costs,
|
|
310
|
+
outputFile: summary.outputFile,
|
|
311
|
+
errors: summary.errors,
|
|
312
|
+
fatalErrorEncountered: isTerminalFailure(status),
|
|
313
|
+
warnings,
|
|
314
|
+
startedAt,
|
|
315
|
+
endedAt
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
async function loadFullHistory(sfn, executionArn) {
|
|
319
|
+
const events = [];
|
|
320
|
+
let nextToken;
|
|
321
|
+
for (let page = 0; page < 50; page++) {
|
|
322
|
+
const res = await sfn.send(
|
|
323
|
+
new GetExecutionHistoryCommand({
|
|
324
|
+
executionArn,
|
|
325
|
+
maxResults: 1e3,
|
|
326
|
+
nextToken,
|
|
327
|
+
reverseOrder: false
|
|
328
|
+
})
|
|
329
|
+
);
|
|
330
|
+
if (res.events) events.push(...res.events);
|
|
331
|
+
nextToken = res.nextToken;
|
|
332
|
+
if (!nextToken) break;
|
|
333
|
+
}
|
|
334
|
+
return events;
|
|
335
|
+
}
|
|
336
|
+
var FAILED_CHUNK_HISTORIES_READ = 5;
|
|
337
|
+
var MAP_RUN_ACCESS_DENIED = "Chunk progress is hidden until the chunks finish: this caller lacks states:DescribeMapRun. Re-apply the policy from `hyperframes lambda policies user`.";
|
|
338
|
+
function findChunkMapRun(events) {
|
|
339
|
+
let run = null;
|
|
340
|
+
for (const ev of events) {
|
|
341
|
+
const arn = ev.mapRunStartedEventDetails?.mapRunArn;
|
|
342
|
+
if (arn) run = { arn, exited: false, output: void 0 };
|
|
343
|
+
const exit = ev.stateExitedEventDetails;
|
|
344
|
+
if (run && ev.type === "MapStateExited" && isRenderChunksMapState(exit?.name)) {
|
|
345
|
+
run = { ...run, exited: true, output: exit?.output };
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return run;
|
|
349
|
+
}
|
|
350
|
+
function addChunkResults(summary, mapOutput, memoryMb) {
|
|
351
|
+
const chunks = parseJson(mapOutput)?.Chunks;
|
|
352
|
+
for (const chunk of Array.isArray(chunks) ? chunks : []) {
|
|
353
|
+
summary.lambdasInvoked++;
|
|
354
|
+
summary.stateTransitions++;
|
|
355
|
+
const billedDurationMs = inferBilledMs(chunk);
|
|
356
|
+
summary.lambdaInvocations.push({
|
|
357
|
+
billedDurationMs,
|
|
358
|
+
memorySizeMb: memoryMb,
|
|
359
|
+
estimated: billedDurationMs === 0
|
|
360
|
+
});
|
|
361
|
+
applyPayloadFrameCounts(chunk, "RenderChunk", (delta) => {
|
|
362
|
+
summary.framesRendered += delta;
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
async function addRunningMapRun(sfn, mapRunArn, summary, memoryMb) {
|
|
367
|
+
const res = await sfn.send(new DescribeMapRunCommand({ mapRunArn }));
|
|
368
|
+
const { total = 0, pending = 0, succeeded = 0, failed = 0 } = res.itemCounts ?? {};
|
|
369
|
+
summary.lambdasInvoked += total - pending;
|
|
370
|
+
summary.stateTransitions += total - pending;
|
|
371
|
+
if (summary.totalFrames !== null && total > 0) {
|
|
372
|
+
summary.framesRendered += Math.floor(summary.totalFrames * succeeded / total);
|
|
373
|
+
}
|
|
374
|
+
if (failed > 0) summary.errors.push(...await failedChunkErrors(sfn, mapRunArn, memoryMb));
|
|
375
|
+
}
|
|
376
|
+
async function failedChunkErrors(sfn, mapRunArn, memoryMb) {
|
|
377
|
+
const failed = await sfn.send(
|
|
378
|
+
new ListExecutionsCommand({
|
|
379
|
+
mapRunArn,
|
|
380
|
+
statusFilter: "FAILED",
|
|
381
|
+
maxResults: FAILED_CHUNK_HISTORIES_READ
|
|
382
|
+
})
|
|
383
|
+
);
|
|
384
|
+
const errors = [];
|
|
385
|
+
for (const child of failed.executions ?? []) {
|
|
386
|
+
if (!child.executionArn) continue;
|
|
387
|
+
const childHistory = await loadFullHistory(sfn, child.executionArn);
|
|
388
|
+
errors.push(...summarizeHistory(childHistory, memoryMb).errors);
|
|
389
|
+
}
|
|
390
|
+
return errors;
|
|
391
|
+
}
|
|
392
|
+
function summarizeHistory(events, memoryMb) {
|
|
393
|
+
let framesRendered = 0;
|
|
394
|
+
let totalFrames = null;
|
|
395
|
+
let lambdasInvoked = 0;
|
|
396
|
+
let assembleComplete = false;
|
|
397
|
+
let outputFile = null;
|
|
398
|
+
let stateTransitions = 0;
|
|
399
|
+
const errors = [];
|
|
400
|
+
const lambdaInvocations = [];
|
|
401
|
+
let currentLambdaState = null;
|
|
402
|
+
for (const ev of events) {
|
|
403
|
+
switch (ev.type) {
|
|
404
|
+
case "TaskStateEntered":
|
|
405
|
+
case "MapStateEntered":
|
|
406
|
+
case "PassStateEntered":
|
|
407
|
+
case "ChoiceStateEntered":
|
|
408
|
+
case "SucceedStateEntered":
|
|
409
|
+
case "FailStateEntered":
|
|
410
|
+
case "WaitStateEntered":
|
|
411
|
+
case "ParallelStateEntered":
|
|
412
|
+
stateTransitions++;
|
|
413
|
+
currentLambdaState = ev.stateEnteredEventDetails?.name ?? currentLambdaState;
|
|
414
|
+
break;
|
|
415
|
+
// Optimized `lambda:invoke` task emits Task* events; raw
|
|
416
|
+
// `lambda:invokeFunction.sync` emits LambdaFunction*. Handle both.
|
|
417
|
+
case "TaskScheduled":
|
|
418
|
+
if (ev.taskScheduledEventDetails?.resourceType === "lambda") {
|
|
419
|
+
lambdasInvoked++;
|
|
420
|
+
}
|
|
421
|
+
break;
|
|
422
|
+
case "LambdaFunctionScheduled":
|
|
423
|
+
lambdasInvoked++;
|
|
424
|
+
break;
|
|
425
|
+
case "TaskSucceeded": {
|
|
426
|
+
if (ev.taskSucceededEventDetails?.resourceType !== "lambda") break;
|
|
427
|
+
const wrapped = parseJson(ev.taskSucceededEventDetails?.output);
|
|
428
|
+
const payload = unwrapLambdaPayload(wrapped);
|
|
429
|
+
const billedDurationMs = inferBilledMs(payload);
|
|
430
|
+
lambdaInvocations.push({
|
|
431
|
+
billedDurationMs,
|
|
432
|
+
memorySizeMb: memoryMb,
|
|
433
|
+
estimated: billedDurationMs === 0
|
|
434
|
+
});
|
|
435
|
+
applyPayloadFrameCounts(payload, currentLambdaState, (delta) => {
|
|
436
|
+
framesRendered += delta;
|
|
437
|
+
});
|
|
438
|
+
if (payload && typeof payload === "object") {
|
|
439
|
+
const obj = payload;
|
|
440
|
+
if (typeof obj.TotalFrames === "number") totalFrames = obj.TotalFrames;
|
|
441
|
+
}
|
|
442
|
+
break;
|
|
443
|
+
}
|
|
444
|
+
case "LambdaFunctionSucceeded": {
|
|
445
|
+
const payload = parseJson(ev.lambdaFunctionSucceededEventDetails?.output);
|
|
446
|
+
const billedDurationMs = inferBilledMs(payload);
|
|
447
|
+
lambdaInvocations.push({
|
|
448
|
+
billedDurationMs,
|
|
449
|
+
memorySizeMb: memoryMb,
|
|
450
|
+
estimated: billedDurationMs === 0
|
|
451
|
+
});
|
|
452
|
+
applyPayloadFrameCounts(payload, currentLambdaState, (delta) => {
|
|
453
|
+
framesRendered += delta;
|
|
454
|
+
});
|
|
455
|
+
if (payload && typeof payload === "object") {
|
|
456
|
+
const obj = payload;
|
|
457
|
+
if (typeof obj.TotalFrames === "number") totalFrames = obj.TotalFrames;
|
|
458
|
+
}
|
|
459
|
+
break;
|
|
460
|
+
}
|
|
461
|
+
case "TaskStateExited":
|
|
462
|
+
case "MapStateExited":
|
|
463
|
+
if (isAssembleState(ev.stateExitedEventDetails?.name)) {
|
|
464
|
+
assembleComplete = true;
|
|
465
|
+
const exitPayload = parseJson(ev.stateExitedEventDetails?.output);
|
|
466
|
+
if (exitPayload && typeof exitPayload === "object") {
|
|
467
|
+
const obj = exitPayload;
|
|
468
|
+
const out = obj.Output;
|
|
469
|
+
const outputS3Uri = typeof out?.OutputS3Uri === "string" ? out.OutputS3Uri : null;
|
|
470
|
+
const bytes = typeof out?.FileSize === "number" ? out.FileSize : null;
|
|
471
|
+
outputFile = outputS3Uri ? { s3Uri: outputS3Uri, bytes } : outputFile;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
break;
|
|
475
|
+
case "TaskFailed":
|
|
476
|
+
if (ev.taskFailedEventDetails?.resourceType !== "lambda") break;
|
|
477
|
+
errors.push({
|
|
478
|
+
state: currentLambdaState ?? "<unknown>",
|
|
479
|
+
error: ev.taskFailedEventDetails?.error ?? "UNKNOWN",
|
|
480
|
+
cause: ev.taskFailedEventDetails?.cause ?? ""
|
|
481
|
+
});
|
|
482
|
+
break;
|
|
483
|
+
case "LambdaFunctionFailed":
|
|
484
|
+
errors.push({
|
|
485
|
+
state: currentLambdaState ?? "<unknown>",
|
|
486
|
+
error: ev.lambdaFunctionFailedEventDetails?.error ?? "UNKNOWN",
|
|
487
|
+
cause: ev.lambdaFunctionFailedEventDetails?.cause ?? ""
|
|
488
|
+
});
|
|
489
|
+
break;
|
|
490
|
+
case "ExecutionFailed":
|
|
491
|
+
errors.push({
|
|
492
|
+
state: "<execution>",
|
|
493
|
+
error: ev.executionFailedEventDetails?.error ?? "UNKNOWN",
|
|
494
|
+
cause: ev.executionFailedEventDetails?.cause ?? ""
|
|
495
|
+
});
|
|
496
|
+
break;
|
|
497
|
+
case "ExecutionAborted":
|
|
498
|
+
errors.push({
|
|
499
|
+
state: "<execution>",
|
|
500
|
+
error: ev.executionAbortedEventDetails?.error ?? "ABORTED",
|
|
501
|
+
cause: ev.executionAbortedEventDetails?.cause ?? ""
|
|
502
|
+
});
|
|
503
|
+
break;
|
|
504
|
+
case "ExecutionTimedOut":
|
|
505
|
+
errors.push({
|
|
506
|
+
state: "<execution>",
|
|
507
|
+
error: "TIMEOUT",
|
|
508
|
+
cause: ev.executionTimedOutEventDetails?.cause ?? ""
|
|
509
|
+
});
|
|
510
|
+
break;
|
|
511
|
+
default:
|
|
512
|
+
break;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
return {
|
|
516
|
+
lambdaInvocations,
|
|
517
|
+
stateTransitions,
|
|
518
|
+
framesRendered,
|
|
519
|
+
totalFrames,
|
|
520
|
+
lambdasInvoked,
|
|
521
|
+
assembleComplete,
|
|
522
|
+
outputFile,
|
|
523
|
+
errors
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
function parseJson(s) {
|
|
527
|
+
if (!s) return null;
|
|
528
|
+
try {
|
|
529
|
+
return JSON.parse(s);
|
|
530
|
+
} catch {
|
|
531
|
+
return null;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
function unwrapLambdaPayload(payload) {
|
|
535
|
+
if (payload && typeof payload === "object" && "Payload" in payload) {
|
|
536
|
+
const inner = payload.Payload;
|
|
537
|
+
if (inner && typeof inner === "object") return inner;
|
|
538
|
+
}
|
|
539
|
+
return payload;
|
|
540
|
+
}
|
|
541
|
+
function applyPayloadFrameCounts(payload, currentLambdaState, bump) {
|
|
542
|
+
if (!isRenderChunkState(currentLambdaState)) return;
|
|
543
|
+
if (!payload || typeof payload !== "object") return;
|
|
544
|
+
const obj = payload;
|
|
545
|
+
if (typeof obj.FramesEncoded === "number") bump(obj.FramesEncoded);
|
|
546
|
+
}
|
|
547
|
+
function isRenderChunkState(name) {
|
|
548
|
+
return name === "RenderChunk" || name === "RenderChunkV2";
|
|
549
|
+
}
|
|
550
|
+
function isRenderChunksMapState(name) {
|
|
551
|
+
return name === "RenderChunks" || name === "RenderChunksV2";
|
|
552
|
+
}
|
|
553
|
+
function isAssembleState(name) {
|
|
554
|
+
return name === "Assemble" || name === "AssembleV2";
|
|
555
|
+
}
|
|
556
|
+
function inferBilledMs(payload) {
|
|
557
|
+
if (!payload || typeof payload !== "object") return 0;
|
|
558
|
+
const obj = payload;
|
|
559
|
+
if (typeof obj.DurationMs === "number") return obj.DurationMs;
|
|
560
|
+
return 0;
|
|
561
|
+
}
|
|
562
|
+
function computeOverallProgress({
|
|
563
|
+
status,
|
|
564
|
+
totalFrames,
|
|
565
|
+
framesRendered,
|
|
566
|
+
assembleComplete
|
|
567
|
+
}) {
|
|
568
|
+
if (status === "SUCCEEDED") return 1;
|
|
569
|
+
if (assembleComplete) return 1;
|
|
570
|
+
if (totalFrames === null) return 0;
|
|
571
|
+
const chunkProgress = Math.min(1, framesRendered / totalFrames);
|
|
572
|
+
return 0.1 + 0.8 * chunkProgress;
|
|
573
|
+
}
|
|
574
|
+
function isTerminalFailure(status) {
|
|
575
|
+
return status === "FAILED" || status === "TIMED_OUT" || status === "ABORTED";
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
export {
|
|
579
|
+
siteHandleFromId,
|
|
580
|
+
deploySite,
|
|
581
|
+
MAX_STEP_FUNCTIONS_INPUT_BYTES,
|
|
582
|
+
validateStepFunctionsInputSize,
|
|
583
|
+
InvalidConfigError2 as InvalidConfigError,
|
|
584
|
+
validateDistributedRenderConfig,
|
|
585
|
+
validateVariablesPayload,
|
|
586
|
+
renderToLambda,
|
|
587
|
+
computeRenderCost,
|
|
588
|
+
getRenderProgress
|
|
589
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// src/contentStore.ts
|
|
2
|
+
var CONTENT_STORE_PREFIX = "cas/sha256";
|
|
3
|
+
var CONTENT_STORE_RETENTION_DAYS = 30;
|
|
4
|
+
var CONTENT_STORE_REFRESH_AFTER_DAYS = 7;
|
|
5
|
+
function contentStoreUri(bucket, sha256) {
|
|
6
|
+
if (!/^[a-f0-9]{64}$/.test(sha256)) {
|
|
7
|
+
throw new Error(
|
|
8
|
+
`[contentStore] expected lowercase SHA-256 digest, got ${JSON.stringify(sha256)}`
|
|
9
|
+
);
|
|
10
|
+
}
|
|
11
|
+
return `s3://${bucket}/${CONTENT_STORE_PREFIX}/${sha256.slice(0, 2)}/${sha256}`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export {
|
|
15
|
+
CONTENT_STORE_PREFIX,
|
|
16
|
+
CONTENT_STORE_RETENTION_DAYS,
|
|
17
|
+
CONTENT_STORE_REFRESH_AFTER_DAYS,
|
|
18
|
+
contentStoreUri
|
|
19
|
+
};
|