@hyperframes/producer 0.2.3 → 0.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/hyperframe.manifest.json +1 -1
- package/dist/hyperframe.runtime.iife.js +5 -5
- package/dist/index.js +200 -30
- package/dist/index.js.map +4 -4
- package/dist/public-server.js +200 -30
- package/dist/public-server.js.map +4 -4
- package/dist/server.d.ts +4 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/services/renderOrchestrator.d.ts.map +1 -1
- package/dist/utils/semaphore.d.ts +16 -0
- package/dist/utils/semaphore.d.ts.map +1 -0
- package/package.json +3 -3
package/dist/public-server.js
CHANGED
|
@@ -102074,7 +102074,8 @@ function truncateSnippet(value, maxLength = 220) {
|
|
|
102074
102074
|
|
|
102075
102075
|
// ../core/src/lint/context.ts
|
|
102076
102076
|
function buildLintContext(html, options = {}) {
|
|
102077
|
-
|
|
102077
|
+
const rawSource = html || "";
|
|
102078
|
+
let source2 = rawSource;
|
|
102078
102079
|
const templateMatch = source2.match(/<template[^>]*>([\s\S]*)<\/template>/i);
|
|
102079
102080
|
if (templateMatch?.[1]) source2 = templateMatch[1];
|
|
102080
102081
|
const tags = extractOpenTags(source2);
|
|
@@ -102085,6 +102086,7 @@ function buildLintContext(html, options = {}) {
|
|
|
102085
102086
|
const rootCompositionId = readAttr(rootTag?.raw || "", "data-composition-id");
|
|
102086
102087
|
return {
|
|
102087
102088
|
source: source2,
|
|
102089
|
+
rawSource,
|
|
102088
102090
|
tags,
|
|
102089
102091
|
styles,
|
|
102090
102092
|
scripts,
|
|
@@ -102875,8 +102877,30 @@ ${right2.raw}`)
|
|
|
102875
102877
|
findings.push({
|
|
102876
102878
|
code: "gsap_infinite_repeat",
|
|
102877
102879
|
severity: "error",
|
|
102878
|
-
message: "GSAP tween uses `repeat: -1` (infinite). Infinite repeats break the deterministic capture engine which seeks to exact frame times. Use a finite repeat count calculated from the composition duration: `repeat: Math.
|
|
102879
|
-
fixHint: "Replace `repeat: -1` with a finite count, e.g. `repeat: Math.
|
|
102880
|
+
message: "GSAP tween uses `repeat: -1` (infinite). Infinite repeats break the deterministic capture engine which seeks to exact frame times. Use a finite repeat count calculated from the composition duration: `repeat: Math.floor(duration / cycleDuration) - 1`.",
|
|
102881
|
+
fixHint: "Replace `repeat: -1` with a finite count, e.g. `repeat: Math.floor(totalDuration / singleCycleDuration) - 1`. Use Math.floor (not Math.ceil) to ensure the animation fits within the total duration.",
|
|
102882
|
+
snippet: truncateSnippet(snippet)
|
|
102883
|
+
});
|
|
102884
|
+
}
|
|
102885
|
+
}
|
|
102886
|
+
return findings;
|
|
102887
|
+
},
|
|
102888
|
+
// gsap_repeat_ceil_overshoot
|
|
102889
|
+
({ scripts }) => {
|
|
102890
|
+
const findings = [];
|
|
102891
|
+
for (const script of scripts) {
|
|
102892
|
+
const content = script.content;
|
|
102893
|
+
const pattern = /repeat\s*:\s*Math\.ceil\s*\([^)]+\)\s*-\s*1/g;
|
|
102894
|
+
let match2;
|
|
102895
|
+
while ((match2 = pattern.exec(content)) !== null) {
|
|
102896
|
+
const contextStart = Math.max(0, match2.index - 40);
|
|
102897
|
+
const contextEnd = Math.min(content.length, match2.index + match2[0].length + 40);
|
|
102898
|
+
const snippet = content.slice(contextStart, contextEnd).trim();
|
|
102899
|
+
findings.push({
|
|
102900
|
+
code: "gsap_repeat_ceil_overshoot",
|
|
102901
|
+
severity: "warning",
|
|
102902
|
+
message: "GSAP repeat calculation uses `Math.ceil` which can overshoot the composition duration. For example, Math.ceil(10.5 / 2) - 1 = 5 repeats \u2192 6 cycles \xD7 2s = 12s, exceeding 10.5s.",
|
|
102903
|
+
fixHint: "Use `Math.floor` instead of `Math.ceil` to ensure the animation fits within the duration: `repeat: Math.floor(totalDuration / cycleDuration) - 1`. Math.floor(10.5 / 2) - 1 = 4 repeats \u2192 5 cycles \xD7 2s = 10s \u2713",
|
|
102880
102904
|
snippet: truncateSnippet(snippet)
|
|
102881
102905
|
});
|
|
102882
102906
|
}
|
|
@@ -103258,6 +103282,76 @@ var compositionRules = [
|
|
|
103258
103282
|
}
|
|
103259
103283
|
return findings;
|
|
103260
103284
|
},
|
|
103285
|
+
// root_composition_missing_data_start
|
|
103286
|
+
({ rootTag }) => {
|
|
103287
|
+
const findings = [];
|
|
103288
|
+
if (!rootTag) return findings;
|
|
103289
|
+
const compId = readAttr(rootTag.raw, "data-composition-id");
|
|
103290
|
+
if (!compId) return findings;
|
|
103291
|
+
const hasStart = readAttr(rootTag.raw, "data-start") !== null;
|
|
103292
|
+
if (!hasStart) {
|
|
103293
|
+
findings.push({
|
|
103294
|
+
code: "root_composition_missing_data_start",
|
|
103295
|
+
severity: "warning",
|
|
103296
|
+
message: `Root composition "${compId}" is missing data-start. The runtime needs data-start="0" on the root element to begin playback.`,
|
|
103297
|
+
fixHint: 'Add data-start="0" to the root composition element.',
|
|
103298
|
+
snippet: truncateSnippet(rootTag.raw)
|
|
103299
|
+
});
|
|
103300
|
+
}
|
|
103301
|
+
return findings;
|
|
103302
|
+
},
|
|
103303
|
+
// root_composition_missing_data_duration
|
|
103304
|
+
({ rootTag }) => {
|
|
103305
|
+
const findings = [];
|
|
103306
|
+
if (!rootTag) return findings;
|
|
103307
|
+
const compId = readAttr(rootTag.raw, "data-composition-id");
|
|
103308
|
+
if (!compId) return findings;
|
|
103309
|
+
const hasDuration = readAttr(rootTag.raw, "data-duration") !== null;
|
|
103310
|
+
if (!hasDuration) {
|
|
103311
|
+
findings.push({
|
|
103312
|
+
code: "root_composition_missing_data_duration",
|
|
103313
|
+
severity: "warning",
|
|
103314
|
+
message: `Root composition "${compId}" is missing data-duration. Without an explicit duration, the runtime may infer Infinity for compositions with repeating animations, causing playback issues.`,
|
|
103315
|
+
fixHint: 'Add data-duration="X" to the root composition element, where X is the total duration in seconds.',
|
|
103316
|
+
snippet: truncateSnippet(rootTag.raw)
|
|
103317
|
+
});
|
|
103318
|
+
}
|
|
103319
|
+
return findings;
|
|
103320
|
+
},
|
|
103321
|
+
// standalone_composition_wrapped_in_template
|
|
103322
|
+
({ rawSource, options }) => {
|
|
103323
|
+
const findings = [];
|
|
103324
|
+
if (options.isSubComposition) return findings;
|
|
103325
|
+
const trimmed = rawSource.trimStart().toLowerCase();
|
|
103326
|
+
if (trimmed.startsWith("<template")) {
|
|
103327
|
+
findings.push({
|
|
103328
|
+
code: "standalone_composition_wrapped_in_template",
|
|
103329
|
+
severity: "warning",
|
|
103330
|
+
message: "Root index.html is wrapped in a <template> tag. Only sub-compositions loaded via data-composition-src should use <template> wrappers. The runtime cannot play a standalone composition inside a template.",
|
|
103331
|
+
fixHint: "Remove the <template> wrapper. Use <!DOCTYPE html><html>...<div data-composition-id>...</div>...</html> instead."
|
|
103332
|
+
});
|
|
103333
|
+
}
|
|
103334
|
+
return findings;
|
|
103335
|
+
},
|
|
103336
|
+
// root_composition_missing_html_wrapper
|
|
103337
|
+
({ rawSource, rootTag, options }) => {
|
|
103338
|
+
const findings = [];
|
|
103339
|
+
if (options.isSubComposition) return findings;
|
|
103340
|
+
const trimmed = rawSource.trimStart().toLowerCase();
|
|
103341
|
+
if (trimmed.startsWith("<template")) return findings;
|
|
103342
|
+
const hasDoctype = trimmed.startsWith("<!doctype") || trimmed.startsWith("<html");
|
|
103343
|
+
const hasComposition = rawSource.includes("data-composition-id");
|
|
103344
|
+
if (hasComposition && !hasDoctype) {
|
|
103345
|
+
findings.push({
|
|
103346
|
+
code: "root_composition_missing_html_wrapper",
|
|
103347
|
+
severity: "error",
|
|
103348
|
+
message: "Composition starts with a bare element instead of a proper HTML document. An index.html that contains data-composition-id but no <!DOCTYPE html>, <html>, or <body> is a fragment \u2014 browsers quirks-mode it, the preview server cannot load it, and the bundler will fail to inject runtime scripts.",
|
|
103349
|
+
fixHint: 'Wrap the composition in <!DOCTYPE html><html><head><meta charset="UTF-8"></head><body>...</body></html>.',
|
|
103350
|
+
snippet: rootTag ? truncateSnippet(rootTag.raw) : void 0
|
|
103351
|
+
});
|
|
103352
|
+
}
|
|
103353
|
+
return findings;
|
|
103354
|
+
},
|
|
103261
103355
|
// requestanimationframe_in_composition
|
|
103262
103356
|
({ scripts }) => {
|
|
103263
103357
|
const findings = [];
|
|
@@ -103439,12 +103533,12 @@ function quantizeTimeToFrame(timeSeconds, fps) {
|
|
|
103439
103533
|
return frameIndex / safeFps;
|
|
103440
103534
|
}
|
|
103441
103535
|
|
|
103442
|
-
// ../../node_modules/.bun/@chenglou+pretext@0.0.
|
|
103536
|
+
// ../../node_modules/.bun/@chenglou+pretext@0.0.5/node_modules/@chenglou/pretext/dist/analysis.js
|
|
103443
103537
|
var arabicScriptRe = new RegExp("\\p{Script=Arabic}", "u");
|
|
103444
103538
|
var combiningMarkRe = new RegExp("\\p{M}", "u");
|
|
103445
103539
|
var decimalDigitRe = new RegExp("\\p{Nd}", "u");
|
|
103446
103540
|
|
|
103447
|
-
// ../../node_modules/.bun/@chenglou+pretext@0.0.
|
|
103541
|
+
// ../../node_modules/.bun/@chenglou+pretext@0.0.5/node_modules/@chenglou/pretext/dist/measurement.js
|
|
103448
103542
|
var emojiPresentationRe = new RegExp("\\p{Emoji_Presentation}", "u");
|
|
103449
103543
|
|
|
103450
103544
|
// ../engine/src/services/screenshotService.ts
|
|
@@ -103458,18 +103552,36 @@ async function getCdpSession(page) {
|
|
|
103458
103552
|
return client;
|
|
103459
103553
|
}
|
|
103460
103554
|
var lastFrameCache = /* @__PURE__ */ new WeakMap();
|
|
103555
|
+
var PENDING_FRAME_RETRIES = 5;
|
|
103556
|
+
async function sendBeginFrame(client, params) {
|
|
103557
|
+
for (let attempt = 0; ; attempt++) {
|
|
103558
|
+
try {
|
|
103559
|
+
return await client.send("HeadlessExperimental.beginFrame", params);
|
|
103560
|
+
} catch (err) {
|
|
103561
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
103562
|
+
const isPending = msg.includes("Another frame is pending");
|
|
103563
|
+
if (isPending && attempt < PENDING_FRAME_RETRIES) {
|
|
103564
|
+
await new Promise((r) => setTimeout(r, 50 * 2 ** attempt));
|
|
103565
|
+
continue;
|
|
103566
|
+
}
|
|
103567
|
+
if (isPending) {
|
|
103568
|
+
throw new Error(
|
|
103569
|
+
`[BeginFrame] Frame still pending after ${PENDING_FRAME_RETRIES} retries \u2014 CPU overloaded by parallel renders. Reduce concurrent renders or use --docker for isolation.`
|
|
103570
|
+
);
|
|
103571
|
+
}
|
|
103572
|
+
throw err;
|
|
103573
|
+
}
|
|
103574
|
+
}
|
|
103575
|
+
}
|
|
103461
103576
|
async function beginFrameCapture(page, options, frameTimeTicks, interval) {
|
|
103462
103577
|
const client = await getCdpSession(page);
|
|
103463
|
-
const
|
|
103464
|
-
const
|
|
103465
|
-
|
|
103466
|
-
|
|
103467
|
-
|
|
103468
|
-
|
|
103469
|
-
|
|
103470
|
-
optimizeForSpeed: true
|
|
103471
|
-
}
|
|
103472
|
-
});
|
|
103578
|
+
const isPng = options.format === "png";
|
|
103579
|
+
const screenshot = {
|
|
103580
|
+
format: isPng ? "png" : "jpeg",
|
|
103581
|
+
quality: isPng ? void 0 : options.quality ?? 80,
|
|
103582
|
+
optimizeForSpeed: true
|
|
103583
|
+
};
|
|
103584
|
+
const result = await sendBeginFrame(client, { frameTimeTicks, interval, screenshot });
|
|
103473
103585
|
let buffer;
|
|
103474
103586
|
if (result.screenshotData) {
|
|
103475
103587
|
buffer = Buffer.from(result.screenshotData, "base64");
|
|
@@ -103479,16 +103591,12 @@ async function beginFrameCapture(page, options, frameTimeTicks, interval) {
|
|
|
103479
103591
|
if (cached) {
|
|
103480
103592
|
buffer = cached;
|
|
103481
103593
|
} else {
|
|
103482
|
-
const
|
|
103594
|
+
const fallback = await sendBeginFrame(client, {
|
|
103483
103595
|
frameTimeTicks: frameTimeTicks + 1e-3,
|
|
103484
103596
|
interval,
|
|
103485
|
-
screenshot
|
|
103486
|
-
format: format3,
|
|
103487
|
-
quality: format3 === "jpeg" ? options.quality ?? 80 : void 0,
|
|
103488
|
-
optimizeForSpeed: true
|
|
103489
|
-
}
|
|
103597
|
+
screenshot
|
|
103490
103598
|
});
|
|
103491
|
-
buffer =
|
|
103599
|
+
buffer = fallback.screenshotData ? Buffer.from(fallback.screenshotData, "base64") : Buffer.alloc(0);
|
|
103492
103600
|
if (buffer.length > 0) lastFrameCache.set(page, buffer);
|
|
103493
103601
|
}
|
|
103494
103602
|
}
|
|
@@ -104991,7 +105099,7 @@ async function extractVideoFramesRange(videoPath, videoId, startTime, duration,
|
|
|
104991
105099
|
});
|
|
104992
105100
|
});
|
|
104993
105101
|
}
|
|
104994
|
-
async function extractAllVideoFrames(videos, baseDir, options, signal, config2) {
|
|
105102
|
+
async function extractAllVideoFrames(videos, baseDir, options, signal, config2, compiledDir) {
|
|
104995
105103
|
const startTime = Date.now();
|
|
104996
105104
|
const extracted = [];
|
|
104997
105105
|
const errors = [];
|
|
@@ -105004,7 +105112,8 @@ async function extractAllVideoFrames(videos, baseDir, options, signal, config2)
|
|
|
105004
105112
|
try {
|
|
105005
105113
|
let videoPath = video.src;
|
|
105006
105114
|
if (!videoPath.startsWith("/") && !isHttpUrl(videoPath)) {
|
|
105007
|
-
|
|
105115
|
+
const fromCompiled = compiledDir ? join8(compiledDir, videoPath) : null;
|
|
105116
|
+
videoPath = fromCompiled && existsSync8(fromCompiled) ? fromCompiled : join8(baseDir, videoPath);
|
|
105008
105117
|
}
|
|
105009
105118
|
if (isHttpUrl(videoPath)) {
|
|
105010
105119
|
const downloadDir = join8(options.outputDir, "_downloads");
|
|
@@ -105465,7 +105574,7 @@ async function mixAudioTracks(tracks, outputPath, totalDuration, signal, config2
|
|
|
105465
105574
|
tracksProcessed: tracks.length
|
|
105466
105575
|
};
|
|
105467
105576
|
}
|
|
105468
|
-
async function processCompositionAudio(elements, baseDir, workDir, outputPath, totalDuration, signal, config2) {
|
|
105577
|
+
async function processCompositionAudio(elements, baseDir, workDir, outputPath, totalDuration, signal, config2, compiledDir) {
|
|
105469
105578
|
const startMs = Date.now();
|
|
105470
105579
|
const tracks = [];
|
|
105471
105580
|
const errors = [];
|
|
@@ -105479,7 +105588,8 @@ async function processCompositionAudio(elements, baseDir, workDir, outputPath, t
|
|
|
105479
105588
|
try {
|
|
105480
105589
|
let srcPath = element.src;
|
|
105481
105590
|
if (!srcPath.startsWith("/") && !isHttpUrl(srcPath)) {
|
|
105482
|
-
|
|
105591
|
+
const fromCompiled = compiledDir ? join9(compiledDir, srcPath) : null;
|
|
105592
|
+
srcPath = fromCompiled && existsSync9(fromCompiled) ? fromCompiled : join9(baseDir, srcPath);
|
|
105483
105593
|
}
|
|
105484
105594
|
if (isHttpUrl(srcPath)) {
|
|
105485
105595
|
try {
|
|
@@ -105492,7 +105602,7 @@ async function processCompositionAudio(elements, baseDir, workDir, outputPath, t
|
|
|
105492
105602
|
}
|
|
105493
105603
|
}
|
|
105494
105604
|
if (!existsSync9(srcPath)) {
|
|
105495
|
-
errors.push(`Source not found: ${element.id}`);
|
|
105605
|
+
errors.push(`Source not found: ${element.id} (${element.src})`);
|
|
105496
105606
|
return;
|
|
105497
105607
|
}
|
|
105498
105608
|
if (element.end - element.start <= 0) {
|
|
@@ -107570,12 +107680,15 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
|
|
|
107570
107680
|
const stage2Start = Date.now();
|
|
107571
107681
|
updateJobStatus(job, "preprocessing", "Extracting video frames", 10, onProgress);
|
|
107572
107682
|
let frameLookup = null;
|
|
107683
|
+
const compiledDir = join13(workDir, "compiled");
|
|
107573
107684
|
if (composition.videos.length > 0) {
|
|
107574
107685
|
const extractionResult = await extractAllVideoFrames(
|
|
107575
107686
|
composition.videos,
|
|
107576
107687
|
projectDir,
|
|
107577
107688
|
{ fps: job.config.fps, outputDir: join13(workDir, "video-frames") },
|
|
107578
|
-
abortSignal
|
|
107689
|
+
abortSignal,
|
|
107690
|
+
void 0,
|
|
107691
|
+
compiledDir
|
|
107579
107692
|
);
|
|
107580
107693
|
assertNotAborted();
|
|
107581
107694
|
if (extractionResult.extracted.length > 0) {
|
|
@@ -107615,7 +107728,9 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
|
|
|
107615
107728
|
join13(workDir, "audio-work"),
|
|
107616
107729
|
audioOutputPath,
|
|
107617
107730
|
job.duration,
|
|
107618
|
-
abortSignal
|
|
107731
|
+
abortSignal,
|
|
107732
|
+
void 0,
|
|
107733
|
+
compiledDir
|
|
107619
107734
|
);
|
|
107620
107735
|
assertNotAborted();
|
|
107621
107736
|
hasAudio = audioResult.success;
|
|
@@ -108118,6 +108233,40 @@ function resolveRenderPaths(projectDir, outputPath, rendersDir = DEFAULT_RENDERS
|
|
|
108118
108233
|
return { absoluteProjectDir, absoluteOutputPath };
|
|
108119
108234
|
}
|
|
108120
108235
|
|
|
108236
|
+
// src/utils/semaphore.ts
|
|
108237
|
+
var Semaphore = class {
|
|
108238
|
+
constructor(maxConcurrent) {
|
|
108239
|
+
this.maxConcurrent = maxConcurrent;
|
|
108240
|
+
}
|
|
108241
|
+
queue = [];
|
|
108242
|
+
active = 0;
|
|
108243
|
+
async acquire() {
|
|
108244
|
+
if (this.active < this.maxConcurrent) {
|
|
108245
|
+
this.active++;
|
|
108246
|
+
return () => this.release();
|
|
108247
|
+
}
|
|
108248
|
+
return new Promise((resolve13) => {
|
|
108249
|
+
this.queue.push(() => {
|
|
108250
|
+
this.active++;
|
|
108251
|
+
resolve13(() => this.release());
|
|
108252
|
+
});
|
|
108253
|
+
});
|
|
108254
|
+
}
|
|
108255
|
+
release() {
|
|
108256
|
+
this.active--;
|
|
108257
|
+
const next = this.queue.shift();
|
|
108258
|
+
if (next) next();
|
|
108259
|
+
}
|
|
108260
|
+
/** Current number of active slots. */
|
|
108261
|
+
get activeCount() {
|
|
108262
|
+
return this.active;
|
|
108263
|
+
}
|
|
108264
|
+
/** Number of waiters in the queue. */
|
|
108265
|
+
get waitingCount() {
|
|
108266
|
+
return this.queue.length;
|
|
108267
|
+
}
|
|
108268
|
+
};
|
|
108269
|
+
|
|
108121
108270
|
// src/server.ts
|
|
108122
108271
|
function parseRenderOptions(body) {
|
|
108123
108272
|
const fps = [24, 30, 60].includes(body.fps) ? body.fps : 30;
|
|
@@ -108231,6 +108380,8 @@ function createRenderHandlers(options = {}) {
|
|
|
108231
108380
|
const rendersDir = options.rendersDir ?? process.env.PRODUCER_RENDERS_DIR ?? "/tmp";
|
|
108232
108381
|
const artifactTtlMs = options.artifactTtlMs ?? Number(process.env.PRODUCER_OUTPUT_ARTIFACT_TTL_MS || 15 * 60 * 1e3);
|
|
108233
108382
|
const store = createArtifactStore(artifactTtlMs);
|
|
108383
|
+
const maxConcurrentRenders = options.maxConcurrentRenders ?? Number(process.env.PRODUCER_MAX_CONCURRENT_RENDERS || 2);
|
|
108384
|
+
const renderSemaphore = new Semaphore(maxConcurrentRenders);
|
|
108234
108385
|
const startTime = Date.now();
|
|
108235
108386
|
const health = (c) => c.json({
|
|
108236
108387
|
status: "ok",
|
|
@@ -108287,6 +108438,7 @@ function createRenderHandlers(options = {}) {
|
|
|
108287
108438
|
);
|
|
108288
108439
|
const outputDir = dirname11(absoluteOutputPath);
|
|
108289
108440
|
if (!existsSync16(outputDir)) mkdirSync10(outputDir, { recursive: true });
|
|
108441
|
+
const release = await renderSemaphore.acquire();
|
|
108290
108442
|
log.info("render started", {
|
|
108291
108443
|
requestId,
|
|
108292
108444
|
projectDir: input2.projectDir,
|
|
@@ -108354,6 +108506,7 @@ function createRenderHandlers(options = {}) {
|
|
|
108354
108506
|
500
|
|
108355
108507
|
);
|
|
108356
108508
|
} finally {
|
|
108509
|
+
release();
|
|
108357
108510
|
cleanupTempDir(cleanupProjectDir, log);
|
|
108358
108511
|
}
|
|
108359
108512
|
};
|
|
@@ -108410,6 +108563,16 @@ function createRenderHandlers(options = {}) {
|
|
|
108410
108563
|
const abortController = new AbortController();
|
|
108411
108564
|
const onRequestAbort = () => abortController.abort(new RenderCancelledError("request_aborted"));
|
|
108412
108565
|
c.req.raw.signal.addEventListener("abort", onRequestAbort, { once: true });
|
|
108566
|
+
if (renderSemaphore.activeCount >= maxConcurrentRenders) {
|
|
108567
|
+
await stream2.writeSSE({
|
|
108568
|
+
data: JSON.stringify({
|
|
108569
|
+
type: "queued",
|
|
108570
|
+
requestId,
|
|
108571
|
+
position: renderSemaphore.waitingCount
|
|
108572
|
+
})
|
|
108573
|
+
});
|
|
108574
|
+
}
|
|
108575
|
+
const release = await renderSemaphore.acquire();
|
|
108413
108576
|
try {
|
|
108414
108577
|
await executeRenderJob(
|
|
108415
108578
|
job,
|
|
@@ -108477,6 +108640,7 @@ function createRenderHandlers(options = {}) {
|
|
|
108477
108640
|
})
|
|
108478
108641
|
});
|
|
108479
108642
|
} finally {
|
|
108643
|
+
release();
|
|
108480
108644
|
c.req.raw.signal.removeEventListener("abort", onRequestAbort);
|
|
108481
108645
|
cleanupTempDir(cleanupProjectDir, log);
|
|
108482
108646
|
}
|
|
@@ -108501,7 +108665,12 @@ function createRenderHandlers(options = {}) {
|
|
|
108501
108665
|
}
|
|
108502
108666
|
});
|
|
108503
108667
|
};
|
|
108504
|
-
|
|
108668
|
+
const queue = (c) => c.json({
|
|
108669
|
+
maxConcurrentRenders,
|
|
108670
|
+
activeRenders: renderSemaphore.activeCount,
|
|
108671
|
+
queuedRenders: renderSemaphore.waitingCount
|
|
108672
|
+
});
|
|
108673
|
+
return { render: render2, renderStream, lint, health, outputs, queue };
|
|
108505
108674
|
}
|
|
108506
108675
|
function createProducerApp(options = {}) {
|
|
108507
108676
|
const app = new Hono2();
|
|
@@ -108509,6 +108678,7 @@ function createProducerApp(options = {}) {
|
|
|
108509
108678
|
app.get("/health", handlers.health);
|
|
108510
108679
|
app.post("/render", handlers.render);
|
|
108511
108680
|
app.post("/render/stream", handlers.renderStream);
|
|
108681
|
+
app.get("/render/queue", handlers.queue);
|
|
108512
108682
|
app.post("/lint", handlers.lint);
|
|
108513
108683
|
app.get("/outputs/:token", handlers.outputs);
|
|
108514
108684
|
return app;
|