@hyperframes/studio-server 0.8.140 → 0.8.141
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/{chunk-G7FEKQ32.js → chunk-5VCIZYUE.js} +8 -7
- package/dist/chunk-5VCIZYUE.js.map +1 -0
- package/dist/{chunk-6RFZWPPN.js → chunk-QP7LKFU2.js} +23 -13
- package/dist/chunk-QP7LKFU2.js.map +1 -0
- package/dist/helpers/mediaProxyPreview.d.ts +2 -2
- package/dist/helpers/mediaProxyPreview.js +2 -2
- package/dist/helpers/proxyTranscoder.d.ts +4 -1
- package/dist/helpers/proxyTranscoder.js +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +10 -4
- package/dist/index.js.map +1 -1
- package/dist/{mediaProxyPreview-CCxjB3rf.d.ts → mediaProxyPreview-CJ4lbPcF.d.ts} +3 -3
- package/package.json +3 -3
- package/dist/chunk-6RFZWPPN.js.map +0 -1
- package/dist/chunk-G7FEKQ32.js.map +0 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
PROXY_PARAMS_VERSION,
|
|
3
3
|
resolveProxy
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-QP7LKFU2.js";
|
|
5
5
|
import {
|
|
6
6
|
createMediaCodecProbeCache,
|
|
7
7
|
proxyVariantFor,
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
|
|
13
13
|
// src/helpers/mediaProxyPreview.ts
|
|
14
14
|
import { resolve } from "path";
|
|
15
|
+
import { formatPreviewProxyBox } from "@hyperframes/core";
|
|
15
16
|
import { insertBeforeCloseTag } from "@hyperframes/core/compiler/html-document";
|
|
16
17
|
function isAutoProxyEnabled(adapter) {
|
|
17
18
|
return adapter.autoProxy !== false;
|
|
@@ -19,11 +20,11 @@ function isAutoProxyEnabled(adapter) {
|
|
|
19
20
|
function resolvePreviewMediaCodecProbeCache(adapter) {
|
|
20
21
|
return adapter.mediaCodecProbeCache ?? createMediaCodecProbeCache();
|
|
21
22
|
}
|
|
22
|
-
function proxyEtagSalt(raw) {
|
|
23
|
+
function proxyEtagSalt(raw, box) {
|
|
23
24
|
if (raw === void 0) return "";
|
|
24
|
-
return `:proxy:${raw}:${PROXY_PARAMS_VERSION}`;
|
|
25
|
+
return `:proxy:${raw}:${PROXY_PARAMS_VERSION}${box ? `:${formatPreviewProxyBox(box)}` : ""}`;
|
|
25
26
|
}
|
|
26
|
-
async function injectMediaCodecMapIntoHtml(html, projectDir, htmlSources, probeCache) {
|
|
27
|
+
async function injectMediaCodecMapIntoHtml(html, projectDir, htmlSources, probeCache, prewarm = true) {
|
|
27
28
|
let map;
|
|
28
29
|
try {
|
|
29
30
|
map = await scanProjectMediaCodecMap(
|
|
@@ -36,7 +37,7 @@ async function injectMediaCodecMapIntoHtml(html, projectDir, htmlSources, probeC
|
|
|
36
37
|
}
|
|
37
38
|
if (Object.keys(map).length === 0) return html;
|
|
38
39
|
for (const [rootRelativePathname, facts] of Object.entries(map)) {
|
|
39
|
-
if (!shouldPrewarmProxy(facts)) continue;
|
|
40
|
+
if (!prewarm || !shouldPrewarmProxy(facts)) continue;
|
|
40
41
|
recordProxyPrewarm();
|
|
41
42
|
resolveProxy(
|
|
42
43
|
projectDir,
|
|
@@ -53,7 +54,7 @@ ${html}`;
|
|
|
53
54
|
}
|
|
54
55
|
async function injectMediaCodecMap(html, adapter, projectDir, compSrcPath, probeCache) {
|
|
55
56
|
if (!isAutoProxyEnabled(adapter)) return html;
|
|
56
|
-
return injectMediaCodecMapIntoHtml(html, projectDir, [{ html, compSrcPath }], probeCache);
|
|
57
|
+
return injectMediaCodecMapIntoHtml(html, projectDir, [{ html, compSrcPath }], probeCache, false);
|
|
57
58
|
}
|
|
58
59
|
|
|
59
60
|
export {
|
|
@@ -63,4 +64,4 @@ export {
|
|
|
63
64
|
injectMediaCodecMapIntoHtml,
|
|
64
65
|
injectMediaCodecMap
|
|
65
66
|
};
|
|
66
|
-
//# sourceMappingURL=chunk-
|
|
67
|
+
//# sourceMappingURL=chunk-5VCIZYUE.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/helpers/mediaProxyPreview.ts"],"sourcesContent":["import { resolve } from \"node:path\";\nimport { formatPreviewProxyBox, type PreviewProxyBox } from \"@hyperframes/core\";\nimport { insertBeforeCloseTag } from \"@hyperframes/core/compiler/html-document\";\nimport type { StudioApiAdapter } from \"../types.js\";\nimport {\n createMediaCodecProbeCache,\n proxyVariantFor,\n recordProxyPrewarm,\n scanProjectMediaCodecMap,\n shouldPrewarmProxy,\n type HtmlSourceLike,\n type MediaCodecMap,\n type MediaCodecProbeCache,\n} from \"./mediaCodecMap.js\";\nimport { resolveProxy, PROXY_PARAMS_VERSION } from \"./proxyTranscoder.js\";\n\n/**\n * Transparent-media-proxy wiring shared by `routes/preview.ts`\n * (docs/plans/2026-07-14-002-feat-transparent-media-proxies-plan.md, unit U3).\n * Split out of the route module to keep it under the repo's 600-line file cap.\n */\n\n/**\n * Preview-route-local adapter surface for the auto-proxy feature. Both\n * fields are optional so any existing `StudioApiAdapter` value remains\n * structurally assignable without editing the shared interface:\n * `autoProxy` defaults to true (on) when omitted — a later unit wires the\n * CLI `--no-proxy` flag / `hyperframes.json` setting through it;\n * `mediaCodecProbeCache` lets a host share one probe cache across\n * preview/play/static-server surfaces instead of each constructing its own.\n */\nexport type PreviewApiAdapter = StudioApiAdapter & {\n autoProxy?: boolean;\n mediaCodecProbeCache?: MediaCodecProbeCache;\n /** Keeps built preview documents across restarts, keyed by project id and preview ETag. */\n previewDocuments?: PreviewDocumentStore;\n};\n\nexport interface PreviewDocumentStore {\n read(key: string): string | null;\n write(key: string, html: string): void;\n}\n\nexport function isAutoProxyEnabled(adapter: PreviewApiAdapter): boolean {\n return adapter.autoProxy !== false;\n}\n\n/** One probe cache per server instance — construct once in `registerPreviewRoutes`\n * and reuse across every request so the mtime-cache benefit in\n * `scanProjectMediaCodecMap` actually applies. A host that wants to share the\n * cache across other surfaces (play, static project server) can pass its own\n * via `adapter.mediaCodecProbeCache`. */\nexport function resolvePreviewMediaCodecProbeCache(\n adapter: PreviewApiAdapter,\n): MediaCodecProbeCache {\n return adapter.mediaCodecProbeCache ?? createMediaCodecProbeCache();\n}\n\n/**\n * ETag salt for `?hf-proxy=` asset requests, mirroring `variablesEtagSalt` in\n * preview.ts: salted by the raw param value plus the transcoder's params\n * version, so a future proxy-recipe change (which bumps `PROXY_PARAMS_VERSION`)\n * or a different proxy variant invalidates cached 304s without needing to\n * touch the proxy file itself.\n */\nexport function proxyEtagSalt(raw: string | undefined, box?: PreviewProxyBox): string {\n if (raw === undefined) return \"\";\n return `:proxy:${raw}:${PROXY_PARAMS_VERSION}${box ? `:${formatPreviewProxyBox(box)}` : \"\"}`;\n}\n\n/**\n * Injects `window.__HF_MEDIA_CODEC_MAP__` (the U1 codec-facts scan) into\n * served composition HTML, and fire-and-forget pre-warms `resolveProxy` for\n * every entry whose codec no browser decodes, so an element's proactive swap\n * usually hits a warm cache (KTD: protects the per-origin connection budget\n * under held responses). Conditionally hostile codecs are injected but NOT\n * pre-warmed: the requesting browser usually plays them, and the transcode\n * runs concurrently with its first layout. No second concurrency limiter here\n * — the transcoder's own global bound throttles both pre-warm and\n * element-triggered calls. Pre-warm failures are swallowed; an actual\n * `?hf-proxy=` request surfaces them as a 502 and transcodes lazily.\n * Alpha-bearing entries pre-warm their VP8/WebM variant.\n *\n * The single shared implementation for every auto-proxy surface — the studio\n * preview route (via `injectMediaCodecMap` below) and the CLI's composition /\n * static project servers (via the `./media-proxy-preview` subpath export).\n * Empty maps leave HTML untouched, preserving the normal no-hostile-media\n * preview path. On-demand proxy requests enforce the same eligibility gate.\n */\nexport async function injectMediaCodecMapIntoHtml(\n html: string,\n projectDir: string,\n htmlSources: HtmlSourceLike[],\n probeCache?: MediaCodecProbeCache,\n prewarm = true,\n): Promise<string> {\n let map: MediaCodecMap;\n try {\n map = await scanProjectMediaCodecMap(\n projectDir,\n htmlSources,\n probeCache ? { cache: probeCache } : {},\n );\n } catch {\n // Best-effort: a scan failure must never block serving the page.\n return html;\n }\n if (Object.keys(map).length === 0) return html;\n for (const [rootRelativePathname, facts] of Object.entries(map)) {\n if (!prewarm || !shouldPrewarmProxy(facts)) continue;\n recordProxyPrewarm();\n resolveProxy(\n projectDir,\n resolve(projectDir, rootRelativePathname.replace(/^\\/+/, \"\")),\n proxyVariantFor(facts),\n ).catch(() => {\n // Swallowed: the pre-warm is best-effort. A real `?hf-proxy=` request\n // for this asset hears the remembered failure (502) or re-attempts it.\n });\n }\n // <-escape prevents a src path containing \"</script>\" from breaking out of\n // the injected tag, mirroring injectPreviewVariables in helpers/previewVariables.ts.\n const json = JSON.stringify(map)\n .replace(/</g, \"\\\\u003c\")\n .replace(/\\u2028/g, \"\\\\u2028\")\n .replace(/\\u2029/g, \"\\\\u2029\");\n const tag = `<script data-hf-media-codec-map>window.__HF_MEDIA_CODEC_MAP__=${json};</script>`;\n return insertBeforeCloseTag(html, \"head\", `${tag}\\n`) ?? `${tag}\\n${html}`;\n}\n\n/**\n * Adapter-aware wrapper used by the studio preview routes: skipped entirely\n * (no scan, no injection) when auto-proxy is off for this adapter.\n */\nexport async function injectMediaCodecMap(\n html: string,\n adapter: PreviewApiAdapter,\n projectDir: string,\n compSrcPath: string,\n probeCache: MediaCodecProbeCache,\n): Promise<string> {\n if (!isAutoProxyEnabled(adapter)) return html;\n return injectMediaCodecMapIntoHtml(html, projectDir, [{ html, compSrcPath }], probeCache, false);\n}\n"],"mappings":";;;;;;;;;;;;;AAAA,SAAS,eAAe;AACxB,SAAS,6BAAmD;AAC5D,SAAS,4BAA4B;AAyC9B,SAAS,mBAAmB,SAAqC;AACtE,SAAO,QAAQ,cAAc;AAC/B;AAOO,SAAS,mCACd,SACsB;AACtB,SAAO,QAAQ,wBAAwB,2BAA2B;AACpE;AASO,SAAS,cAAc,KAAyB,KAA+B;AACpF,MAAI,QAAQ,OAAW,QAAO;AAC9B,SAAO,UAAU,GAAG,IAAI,oBAAoB,GAAG,MAAM,IAAI,sBAAsB,GAAG,CAAC,KAAK,EAAE;AAC5F;AAqBA,eAAsB,4BACpB,MACA,YACA,aACA,YACA,UAAU,MACO;AACjB,MAAI;AACJ,MAAI;AACF,UAAM,MAAM;AAAA,MACV;AAAA,MACA;AAAA,MACA,aAAa,EAAE,OAAO,WAAW,IAAI,CAAC;AAAA,IACxC;AAAA,EACF,QAAQ;AAEN,WAAO;AAAA,EACT;AACA,MAAI,OAAO,KAAK,GAAG,EAAE,WAAW,EAAG,QAAO;AAC1C,aAAW,CAAC,sBAAsB,KAAK,KAAK,OAAO,QAAQ,GAAG,GAAG;AAC/D,QAAI,CAAC,WAAW,CAAC,mBAAmB,KAAK,EAAG;AAC5C,uBAAmB;AACnB;AAAA,MACE;AAAA,MACA,QAAQ,YAAY,qBAAqB,QAAQ,QAAQ,EAAE,CAAC;AAAA,MAC5D,gBAAgB,KAAK;AAAA,IACvB,EAAE,MAAM,MAAM;AAAA,IAGd,CAAC;AAAA,EACH;AAGA,QAAM,OAAO,KAAK,UAAU,GAAG,EAC5B,QAAQ,MAAM,SAAS,EACvB,QAAQ,WAAW,SAAS,EAC5B,QAAQ,WAAW,SAAS;AAC/B,QAAM,MAAM,iEAAiE,IAAI;AACjF,SAAO,qBAAqB,MAAM,QAAQ,GAAG,GAAG;AAAA,CAAI,KAAK,GAAG,GAAG;AAAA,EAAK,IAAI;AAC1E;AAMA,eAAsB,oBACpB,MACA,SACA,YACA,aACA,YACiB;AACjB,MAAI,CAAC,mBAAmB,OAAO,EAAG,QAAO;AACzC,SAAO,4BAA4B,MAAM,YAAY,CAAC,EAAE,MAAM,YAAY,CAAC,GAAG,YAAY,KAAK;AACjG;","names":[]}
|
|
@@ -14,7 +14,10 @@ import { spawn } from "child_process";
|
|
|
14
14
|
import { createHash, randomUUID } from "crypto";
|
|
15
15
|
import { existsSync as existsSync2, renameSync, statSync as statSync2, unlinkSync as unlinkSync2, utimesSync } from "fs";
|
|
16
16
|
import { basename, dirname, isAbsolute, join as join2, relative, resolve, sep } from "path";
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
formatPreviewProxyBox,
|
|
19
|
+
hdrToSdrToneMapFilter
|
|
20
|
+
} from "@hyperframes/core";
|
|
18
21
|
import { findFfBinary } from "@hyperframes/parsers/ff-binaries";
|
|
19
22
|
|
|
20
23
|
// src/helpers/proxyCache.ts
|
|
@@ -211,14 +214,15 @@ function canonicalizeProxySource(projectDir, absoluteSourcePath) {
|
|
|
211
214
|
cacheIdentity: (sourceIsInsideCanonicalProject ? canonicalRelativePath : canonicalSourcePath).normalize("NFC")
|
|
212
215
|
};
|
|
213
216
|
}
|
|
214
|
-
function buildProxyCacheKey(source, variant) {
|
|
217
|
+
function buildProxyCacheKey(source, variant, box) {
|
|
215
218
|
const stat = statSync2(source.sourcePath);
|
|
219
|
+
const size = box ? `\0${formatPreviewProxyBox(box)}` : "";
|
|
216
220
|
return createHash("sha256").update(
|
|
217
|
-
`${source.relativePath}\0${source.cacheIdentity}\0${stat.mtimeMs}\0${stat.size}\0${PROXY_PARAMS_VERSION}\0${variant}`
|
|
221
|
+
`${source.relativePath}\0${source.cacheIdentity}\0${stat.mtimeMs}\0${stat.size}\0${PROXY_PARAMS_VERSION}\0${variant}${size}`
|
|
218
222
|
).digest("hex");
|
|
219
223
|
}
|
|
220
|
-
function getCanonicalProxyCachePath(source, variant) {
|
|
221
|
-
const key = buildProxyCacheKey(source, variant);
|
|
224
|
+
function getCanonicalProxyCachePath(source, variant, box) {
|
|
225
|
+
const key = buildProxyCacheKey(source, variant, box);
|
|
222
226
|
return join2(
|
|
223
227
|
source.projectDir,
|
|
224
228
|
CACHE_DIR_NAME,
|
|
@@ -310,7 +314,13 @@ function rememberFailure(cachePath, error) {
|
|
|
310
314
|
function clearFailedTranscodesForTest() {
|
|
311
315
|
failedTranscodes.clear();
|
|
312
316
|
}
|
|
313
|
-
|
|
317
|
+
function proxyScaleFilter(box) {
|
|
318
|
+
if (!box) return "scale=trunc(iw/2)*2:trunc(ih/2)*2";
|
|
319
|
+
const factor = `min(1\\,max(${box.width}/iw\\,${box.height}/ih))`;
|
|
320
|
+
const side = (d) => `min(trunc(${d}/2)*2\\,ceil(${d}*${factor}/2)*2)`;
|
|
321
|
+
return `scale=${side("iw")}:${side("ih")}`;
|
|
322
|
+
}
|
|
323
|
+
async function runFfmpeg(sourcePath, outputPath, variant, box) {
|
|
314
324
|
const metadata = await probeMediaMetadata(sourcePath);
|
|
315
325
|
const ffmpegPath = findFfBinary("ffmpeg", { configuredMustExist: true });
|
|
316
326
|
if (!ffmpegPath) {
|
|
@@ -321,7 +331,7 @@ async function runFfmpeg(sourcePath, outputPath, variant) {
|
|
|
321
331
|
const toneMap = (hdrTransfer === "pq" || hdrTransfer === "hlg") && !keepsAlpha;
|
|
322
332
|
if (toneMap) await ensureHdrFilters(ffmpegPath);
|
|
323
333
|
const firstFrame = toneMap ? await probeFirstFrameColour(sourcePath) : {};
|
|
324
|
-
const evenScale =
|
|
334
|
+
const evenScale = proxyScaleFilter(box);
|
|
325
335
|
const pixelFormat = keepsAlpha ? "yuva420p" : "yuv420p";
|
|
326
336
|
const videoFilter = toneMap ? [
|
|
327
337
|
hdrToSdrToneMapFilter(metadata.color, firstFrame),
|
|
@@ -411,7 +421,7 @@ async function runFfmpeg(sourcePath, outputPath, variant) {
|
|
|
411
421
|
});
|
|
412
422
|
});
|
|
413
423
|
}
|
|
414
|
-
async function transcodeToCache(projectDir, absoluteSourcePath, cachePath, variant) {
|
|
424
|
+
async function transcodeToCache(projectDir, absoluteSourcePath, cachePath, variant, box) {
|
|
415
425
|
await acquireSlot();
|
|
416
426
|
try {
|
|
417
427
|
if (existsSync2(cachePath)) return cachePath;
|
|
@@ -419,7 +429,7 @@ async function transcodeToCache(projectDir, absoluteSourcePath, cachePath, varia
|
|
|
419
429
|
mkdirWithinProject(projectDir, cacheDir);
|
|
420
430
|
const tempPath = join2(cacheDir, `.tmp-${randomUUID()}-${basename(cachePath)}`);
|
|
421
431
|
try {
|
|
422
|
-
await runFfmpeg(absoluteSourcePath, tempPath, variant);
|
|
432
|
+
await runFfmpeg(absoluteSourcePath, tempPath, variant, box);
|
|
423
433
|
renameSync(tempPath, cachePath);
|
|
424
434
|
maintainProxyCache(cacheDir);
|
|
425
435
|
return cachePath;
|
|
@@ -437,9 +447,9 @@ function proxyActivityMark(projectDir) {
|
|
|
437
447
|
for (const cachePath of inFlight.keys()) if (cachePath.startsWith(cacheDir)) return null;
|
|
438
448
|
return String(settledProxyCount);
|
|
439
449
|
}
|
|
440
|
-
async function resolveProxy(projectDir, absoluteSourcePath, variant = "h264") {
|
|
450
|
+
async function resolveProxy(projectDir, absoluteSourcePath, variant = "h264", box) {
|
|
441
451
|
const source = canonicalizeProxySource(projectDir, absoluteSourcePath);
|
|
442
|
-
const cachePath = getCanonicalProxyCachePath(source, variant);
|
|
452
|
+
const cachePath = getCanonicalProxyCachePath(source, variant, box);
|
|
443
453
|
if (existsSync2(cachePath)) {
|
|
444
454
|
markCacheEntryUsed(cachePath);
|
|
445
455
|
maintainProxyCache(dirname(cachePath));
|
|
@@ -452,7 +462,7 @@ async function resolveProxy(projectDir, absoluteSourcePath, variant = "h264") {
|
|
|
452
462
|
}
|
|
453
463
|
const existing = inFlight.get(cachePath);
|
|
454
464
|
if (existing) return existing;
|
|
455
|
-
const promise = transcodeToCache(source.projectDir, source.sourcePath, cachePath, variant).catch((err) => {
|
|
465
|
+
const promise = transcodeToCache(source.projectDir, source.sourcePath, cachePath, variant, box).catch((err) => {
|
|
456
466
|
if (!(err instanceof ProxyCapacityError)) rememberFailure(cachePath, err);
|
|
457
467
|
throw err;
|
|
458
468
|
}).finally(() => {
|
|
@@ -481,4 +491,4 @@ export {
|
|
|
481
491
|
proxyActivityMark,
|
|
482
492
|
resolveProxy
|
|
483
493
|
};
|
|
484
|
-
//# sourceMappingURL=chunk-
|
|
494
|
+
//# sourceMappingURL=chunk-QP7LKFU2.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/helpers/proxyTranscoder.ts","../src/helpers/proxyCache.ts"],"sourcesContent":["import { spawn } from \"node:child_process\";\nimport { createHash, randomUUID } from \"node:crypto\";\nimport { existsSync, renameSync, statSync, unlinkSync, utimesSync } from \"node:fs\";\nimport { basename, dirname, isAbsolute, join, relative, resolve, sep } from \"node:path\";\nimport {\n formatPreviewProxyBox,\n hdrToSdrToneMapFilter,\n type PreviewProxyBox,\n} from \"@hyperframes/core\";\nimport { findFfBinary } from \"@hyperframes/parsers/ff-binaries\";\nimport { probeFirstFrameColour, probeMediaMetadata } from \"./mediaMetadata.js\";\nimport { cleanupProxyCache } from \"./proxyCache.js\";\nimport { PROXY_VARIANT_CONFIG, type ProxyVariant } from \"./mediaCodecMap.js\";\nimport { mkdirWithinProject, realpath, realProjectRoot } from \"./safePath.js\";\n\n/**\n * Transcodes browser-hostile local video sources (HEVC, ProRes, ...) into a\n * cached, seekable authoring proxy. Consumed by the preview/play/static\n * project routes (U3/U4) to serve a `?hf-proxy=` request; never used on\n * the render path (render always sees the original file).\n *\n * IMPORTANT — request-lifecycle detachment: nothing here accepts or wires an\n * AbortSignal. `resolveProxy` returns a promise shared by every concurrent\n * caller for the same cache key (in-flight dedupe below); if a route handler\n * killed the ffmpeg child on client abort (page reload, HMR), every other\n * caller waiting on that same promise would fail too, and the next request\n * would restart a transcode that may have been minutes into a long asset.\n * Callers MUST let the child run to completion regardless of request\n * cancellation and simply let the held response also abort — the cache\n * entry still lands for the next request.\n */\n\nexport const PROXY_PARAMS_VERSION = \"v5\";\n\nexport const CACHE_DIR_NAME = \".transcode-cache\";\n\nfunction boundedEnvInteger(name: string, fallback: number, min: number, max: number): number {\n const raw = process.env[name]?.trim();\n if (!raw) return fallback;\n const parsed = Number(raw);\n return Number.isSafeInteger(parsed) && parsed >= min && parsed <= max ? parsed : fallback;\n}\n\n// ffmpeg is internally multithreaded, so two concurrent proxy encodes already\n// saturate a typical laptop. Operators of shared/large machines may tune the\n// bounded values without patching the package; invalid values fail safe.\nconst MAX_CONCURRENT_TRANSCODES = boundedEnvInteger(\"HYPERFRAMES_PROXY_MAX_CONCURRENCY\", 2, 1, 16);\nconst MAX_QUEUED_TRANSCODES = boundedEnvInteger(\"HYPERFRAMES_PROXY_MAX_QUEUE\", 8, 0, 256);\n\nconst STDERR_TAIL_MAX_CHARS = 4000;\nexport const TRANSCODE_TIMEOUT_MS = 15 * 60 * 1000;\nconst FAILURE_CACHE_TTL_MS = 60 * 1000;\nexport const PROXY_PENDING_RETRY_AFTER_SECONDS = 2;\nconst ENVIRONMENT_FAILURE_TTL_MS = 5 * PROXY_PENDING_RETRY_AFTER_SECONDS * 1000;\nconst MAX_FAILURE_CACHE_ENTRIES = 128;\nexport const DEFAULT_PROXY_WAIT_TIMEOUT_MS = 2 * 60 * 1000;\n\nexport class ProxyTranscodeError extends Error {\n readonly exitCode: number | null;\n readonly stderrTail: string;\n\n constructor(message: string, exitCode: number | null, stderrTail: string) {\n super(message);\n this.name = \"ProxyTranscodeError\";\n this.exitCode = exitCode;\n this.stderrTail = stderrTail;\n }\n}\n\n/** \"ffmpeg isn't installed\" — an environment condition, not a per-source\n * failure, so the negative cache below keeps it only briefly\n * (installing ffmpeg mid-session must recover without a server restart). */\nclass FfmpegUnavailableError extends ProxyTranscodeError {\n constructor() {\n super(\"ffmpeg binary not found\", null, \"\");\n }\n}\n\nexport class FfmpegMissingFilterError extends ProxyTranscodeError {\n constructor() {\n super(\n \"HDR proxying requires ffmpeg zscale/tonemap filters (libzimg); install an ffmpeg build with libzimg support\",\n null,\n \"\",\n );\n this.name = \"FfmpegMissingFilterError\";\n }\n}\n\nexport class ProxyCapacityError extends ProxyTranscodeError {\n constructor() {\n super(\"media proxy queue is full; retry shortly\", null, \"\");\n this.name = \"ProxyCapacityError\";\n }\n}\n\nexport class ProxySourceOutsideProjectError extends ProxyTranscodeError {\n constructor() {\n super(\"media proxy source must be addressed through the project\", null, \"\");\n this.name = \"ProxySourceOutsideProjectError\";\n }\n}\n\nexport class ProxyWaitTimeoutError extends ProxyTranscodeError {\n constructor(timeoutMs: number) {\n super(`media proxy did not become ready within ${timeoutMs}ms`, null, \"\");\n this.name = \"ProxyWaitTimeoutError\";\n }\n}\n\n/** Bounds one caller's wait without cancelling the shared in-flight ffmpeg\n * job. Other preview/publish callers still receive the completed cache entry. */\nexport async function waitForProxy<T>(\n promise: Promise<T>,\n timeoutMs = DEFAULT_PROXY_WAIT_TIMEOUT_MS,\n): Promise<T> {\n let timer: NodeJS.Timeout | undefined;\n try {\n return await Promise.race([\n promise,\n new Promise<never>((_resolve, reject) => {\n timer = setTimeout(() => reject(new ProxyWaitTimeoutError(timeoutMs)), timeoutMs);\n timer.unref();\n }),\n ]);\n } finally {\n if (timer) clearTimeout(timer);\n }\n}\n\n/**\n * Cache key inputs per the plan: project-relative source path (portable across\n * checkouts), canonical source identity (so retargeted external symlinks do\n * not reuse a proxy), mtime and file size (mtime alone can collide on\n * same-second re-exports on coarse-timestamp filesystems; size catches nearly\n * all such cases at zero cost), and a params version token so changing the\n * ffmpeg recipe below invalidates every cached proxy cleanly.\n */\ntype CanonicalProxySource = {\n projectDir: string;\n sourcePath: string;\n relativePath: string;\n cacheIdentity: string;\n};\n\nfunction canonicalizeProxySource(\n projectDir: string,\n absoluteSourcePath: string,\n): CanonicalProxySource {\n const requestedProjectDir = resolve(projectDir);\n const requestedSourcePath = resolve(absoluteSourcePath);\n const requestedRelativePath = relative(requestedProjectDir, requestedSourcePath);\n if (\n requestedRelativePath === \"..\" ||\n requestedRelativePath.startsWith(`..${sep}`) ||\n isAbsolute(requestedRelativePath)\n ) {\n throw new ProxySourceOutsideProjectError();\n }\n\n const canonicalProjectDir = realProjectRoot(projectDir);\n const canonicalSourcePath = realpath(absoluteSourcePath);\n const canonicalRelativePath = relative(canonicalProjectDir, canonicalSourcePath);\n const sourceIsInsideCanonicalProject =\n canonicalRelativePath !== \"..\" &&\n !canonicalRelativePath.startsWith(`..${sep}`) &&\n !isAbsolute(canonicalRelativePath);\n return {\n projectDir: canonicalProjectDir,\n sourcePath: canonicalSourcePath,\n relativePath: requestedRelativePath.normalize(\"NFC\"),\n // An external target needs a stable identity in addition to its project-local\n // symlink path, otherwise retargeting the link can reuse an unrelated proxy.\n cacheIdentity: (sourceIsInsideCanonicalProject\n ? canonicalRelativePath\n : canonicalSourcePath\n ).normalize(\"NFC\"),\n };\n}\n\nfunction buildProxyCacheKey(\n source: CanonicalProxySource,\n variant: ProxyVariant,\n box: PreviewProxyBox | undefined,\n): string {\n const stat = statSync(source.sourcePath);\n const size = box ? `\\0${formatPreviewProxyBox(box)}` : \"\";\n return createHash(\"sha256\")\n .update(\n `${source.relativePath}\\0${source.cacheIdentity}\\0${stat.mtimeMs}\\0${stat.size}\\0${PROXY_PARAMS_VERSION}\\0${variant}${size}`,\n )\n .digest(\"hex\");\n}\n\nfunction getCanonicalProxyCachePath(\n source: CanonicalProxySource,\n variant: ProxyVariant,\n box?: PreviewProxyBox,\n): string {\n const key = buildProxyCacheKey(source, variant, box);\n return join(\n source.projectDir,\n CACHE_DIR_NAME,\n `${key}${PROXY_VARIANT_CONFIG[variant].extension}`,\n );\n}\n\n/**\n * Computes the absolute path a proxy for this source would live at, without\n * transcoding anything. Route handlers use this to check cache state (e.g.\n * for ETag/If-None-Match) before deciding whether to await a transcode.\n */\nexport function getProxyCachePath(\n projectDir: string,\n absoluteSourcePath: string,\n variant: ProxyVariant = \"h264\",\n): string {\n return getCanonicalProxyCachePath(\n canonicalizeProxySource(projectDir, absoluteSourcePath),\n variant,\n );\n}\n\n// --- global concurrency limiter -------------------------------------------\n// ponytail: a bare counter + FIFO wait queue is the whole semaphore; no\n// dependency pulled in for this. Both element-triggered and pre-warm calls\n// go through the same `resolveProxy` entry point, so both queue here.\n\nlet activeTranscodes = 0;\nconst waitQueue: Array<() => void> = [];\n\nfunction acquireSlot(): Promise<void> {\n return new Promise((resolveSlot, reject) => {\n const tryAcquire = (): void => {\n if (activeTranscodes < MAX_CONCURRENT_TRANSCODES) {\n activeTranscodes++;\n resolveSlot();\n } else {\n if (waitQueue.length >= MAX_QUEUED_TRANSCODES) {\n reject(new ProxyCapacityError());\n return;\n }\n waitQueue.push(tryAcquire);\n }\n };\n tryAcquire();\n });\n}\n\nfunction releaseSlot(): void {\n activeTranscodes--;\n const next = waitQueue.shift();\n if (next) next();\n}\n\n// --- per-key in-flight dedupe ----------------------------------------------\n\nconst inFlight = new Map<string, Promise<string>>();\n\nfunction maintainProxyCache(cacheDir: string): void {\n try {\n cleanupProxyCache(cacheDir, { protectedPaths: new Set(inFlight.keys()) });\n } catch (error) {\n // Cache maintenance must never turn a playable preview into an error.\n console.warn(\n `[media-proxy] cache cleanup failed: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n}\n\nfunction markCacheEntryUsed(cachePath: string): void {\n try {\n const now = new Date();\n utimesSync(cachePath, now, now);\n } catch {\n // A concurrent cleanup may have removed a stale entry after existsSync;\n // the normal miss path below will recreate it on the next request.\n }\n}\n\n// --- negative cache ---------------------------------------------------------\n// A source that failed to transcode fails again identically until the file\n// changes (the cache key embeds mtime+size, so a re-export invalidates this\n// naturally). Remembering the failure per key means repeated `?hf-proxy=`\n// requests for a broken asset rethrow instantly instead of respawning ffmpeg\n// on every retry the browser makes.\ninterface RememberedFailure {\n error: unknown;\n expiresAt: number;\n}\n\nconst failedTranscodes = new Map<string, RememberedFailure>();\n\nlet hdrFilterCheck: { ffmpegPath: string; promise: Promise<void> } | undefined;\n\nfunction ensureHdrFilters(ffmpegPath: string): Promise<void> {\n if (hdrFilterCheck?.ffmpegPath === ffmpegPath) return hdrFilterCheck.promise;\n const promise = new Promise<void>((resolveCheck, rejectCheck) => {\n const proc = spawn(ffmpegPath, [\"-hide_banner\", \"-filters\"], {\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n windowsHide: true,\n });\n let stdout = \"\";\n proc.stdout?.on(\"data\", (chunk: Buffer) => {\n stdout += chunk.toString();\n });\n proc.on(\"error\", () => rejectCheck(new FfmpegMissingFilterError()));\n proc.on(\"close\", (code) => {\n if (code !== 0 || !/\\bzscale\\b/.test(stdout) || !/\\btonemap\\b/.test(stdout)) {\n rejectCheck(new FfmpegMissingFilterError());\n } else {\n resolveCheck();\n }\n });\n });\n hdrFilterCheck = { ffmpegPath, promise };\n return promise;\n}\n\nfunction rememberFailure(cachePath: string, error: unknown): void {\n const ttlMs =\n error instanceof FfmpegUnavailableError || error instanceof FfmpegMissingFilterError\n ? ENVIRONMENT_FAILURE_TTL_MS\n : FAILURE_CACHE_TTL_MS;\n failedTranscodes.delete(cachePath);\n failedTranscodes.set(cachePath, { error, expiresAt: Date.now() + ttlMs });\n while (failedTranscodes.size > MAX_FAILURE_CACHE_ENTRIES) {\n const oldest = failedTranscodes.keys().next().value;\n if (oldest === undefined) break;\n failedTranscodes.delete(oldest);\n }\n}\n\n/** Test hook: forget remembered transcode failures (module state persists\n * across tests that don't reload the module). */\nexport function clearFailedTranscodesForTest(): void {\n failedTranscodes.clear();\n}\n\n/** Even dimensions; with a box, the source shrinks until its tighter side fills the box, never grows. */\nfunction proxyScaleFilter(box?: PreviewProxyBox): string {\n if (!box) return \"scale=trunc(iw/2)*2:trunc(ih/2)*2\";\n const factor = `min(1\\\\,max(${box.width}/iw\\\\,${box.height}/ih))`;\n const side = (d: string) => `min(trunc(${d}/2)*2\\\\,ceil(${d}*${factor}/2)*2)`;\n return `scale=${side(\"iw\")}:${side(\"ih\")}`;\n}\n\nasync function runFfmpeg(\n sourcePath: string,\n outputPath: string,\n variant: ProxyVariant,\n box: PreviewProxyBox | undefined,\n): Promise<void> {\n const metadata = await probeMediaMetadata(sourcePath);\n const ffmpegPath = findFfBinary(\"ffmpeg\", { configuredMustExist: true });\n if (!ffmpegPath) {\n throw new FfmpegUnavailableError();\n }\n const keepsAlpha = variant === \"vp8\";\n const { hdrTransfer } = metadata.color;\n const toneMap = (hdrTransfer === \"pq\" || hdrTransfer === \"hlg\") && !keepsAlpha;\n if (toneMap) await ensureHdrFilters(ffmpegPath);\n const firstFrame = toneMap ? await probeFirstFrameColour(sourcePath) : {};\n const evenScale = proxyScaleFilter(box);\n const pixelFormat = keepsAlpha ? \"yuva420p\" : \"yuv420p\";\n // The tone map ends in RGB; older ffmpeg (seen on 5.1) converts it with BT.601 unless the matrix is named.\n const videoFilter = toneMap\n ? [\n hdrToSdrToneMapFilter(metadata.color, firstFrame),\n `${evenScale}:out_color_matrix=bt709:out_range=tv`,\n `format=${pixelFormat}`,\n ].join(\",\")\n : [evenScale, `format=${pixelFormat}`].join(\",\");\n\n return new Promise((resolvePromise, reject) => {\n const commonArgs = [\"-y\", \"-i\", sourcePath, \"-vf\", videoFilter];\n const h264Args = [\n \"-c:v\",\n \"libx264\",\n \"-profile:v\",\n \"high\",\n \"-pix_fmt\",\n \"yuv420p\",\n \"-colorspace\",\n \"bt709\",\n \"-color_primaries\",\n \"bt709\",\n \"-color_trc\",\n \"bt709\",\n \"-crf\",\n \"18\",\n \"-preset\",\n \"veryfast\",\n \"-c:a\",\n \"aac\",\n \"-movflags\",\n \"+faststart\",\n ];\n const vp8Args = [\n \"-c:v\",\n \"libvpx\",\n \"-b:v\",\n \"0\",\n \"-crf\",\n \"23\",\n \"-deadline\",\n \"good\",\n \"-pix_fmt\",\n \"yuva420p\",\n \"-colorspace\",\n \"bt709\",\n \"-color_primaries\",\n \"bt709\",\n \"-color_trc\",\n \"bt709\",\n \"-cpu-used\",\n \"4\",\n \"-auto-alt-ref\",\n \"0\",\n \"-metadata:s:v:0\",\n \"alpha_mode=1\",\n \"-ac\",\n \"2\",\n \"-c:a\",\n \"libopus\",\n ];\n const args = [...commonArgs, ...(variant === \"vp8\" ? vp8Args : h264Args), outputPath];\n\n // Hard ceiling so a hung ffmpeg can never permanently occupy one of the\n // global transcode slots: the child is killed and the slot released via\n // the caller's finally. Generous because long assets transcode at\n // roughly real time; a healthy encode of any authoring asset fits.\n const proc = spawn(ffmpegPath, args, {\n stdio: [\"ignore\", \"ignore\", \"pipe\"],\n timeout: TRANSCODE_TIMEOUT_MS,\n killSignal: \"SIGKILL\",\n windowsHide: true,\n });\n let stderrTail = \"\";\n proc.stderr?.on(\"data\", (chunk: Buffer) => {\n stderrTail = (stderrTail + chunk.toString()).slice(-STDERR_TAIL_MAX_CHARS);\n });\n proc.on(\"error\", (err) => {\n reject(new ProxyTranscodeError(`failed to spawn ffmpeg: ${err.message}`, null, stderrTail));\n });\n proc.on(\"close\", (code, signal) => {\n if (code === 0) {\n resolvePromise();\n } else if (signal) {\n reject(\n new ProxyTranscodeError(\n `ffmpeg killed by ${signal} (timeout ${TRANSCODE_TIMEOUT_MS}ms or external kill)`,\n null,\n stderrTail,\n ),\n );\n } else {\n reject(new ProxyTranscodeError(`ffmpeg exited with code ${code}`, code, stderrTail));\n }\n });\n });\n}\n\nasync function transcodeToCache(\n projectDir: string,\n absoluteSourcePath: string,\n cachePath: string,\n variant: ProxyVariant,\n box: PreviewProxyBox | undefined,\n): Promise<string> {\n await acquireSlot();\n try {\n // Another caller may have finished (or a pre-warm beat us) while queued.\n if (existsSync(cachePath)) return cachePath;\n\n const cacheDir = dirname(cachePath);\n mkdirWithinProject(projectDir, cacheDir);\n const tempPath = join(cacheDir, `.tmp-${randomUUID()}-${basename(cachePath)}`);\n try {\n await runFfmpeg(absoluteSourcePath, tempPath, variant, box);\n renameSync(tempPath, cachePath);\n maintainProxyCache(cacheDir);\n return cachePath;\n } finally {\n // No partial files: if anything above threw, remove whatever ffmpeg\n // may have partially written under the temp name.\n if (existsSync(tempPath)) unlinkSync(tempPath);\n }\n } finally {\n releaseSlot();\n }\n}\n\nlet settledProxyCount = 0;\n\n/** Null while a copy for this project is being made; otherwise a mark that moves when any copy finishes. */\nexport function proxyActivityMark(projectDir: string): string | null {\n if (!existsSync(projectDir)) return String(settledProxyCount);\n const cacheDir = join(realpath(projectDir), CACHE_DIR_NAME) + sep;\n for (const cachePath of inFlight.keys()) if (cachePath.startsWith(cacheDir)) return null;\n return String(settledProxyCount);\n}\n\n/**\n * Resolves the cached proxy variant for `absoluteSourcePath`, transcoding it at\n * most once per cache key. Concurrent calls for the same key (including a\n * pre-warm call racing an element-triggered one) share one ffmpeg child and\n * one promise; calls for different keys queue through the global concurrency\n * limiter above. Throws `ProxyTranscodeError` on failure (missing ffmpeg or a\n * nonzero exit) — callers (route handlers) decide how to surface that (502).\n * A `box` makes a smaller preview-only copy with its own cache entry; without\n * one the copy keeps the source size (CLI play, static servers, publish).\n */\nexport async function resolveProxy(\n projectDir: string,\n absoluteSourcePath: string,\n variant: ProxyVariant = \"h264\",\n box?: PreviewProxyBox,\n): Promise<string> {\n const source = canonicalizeProxySource(projectDir, absoluteSourcePath);\n const cachePath = getCanonicalProxyCachePath(source, variant, box);\n if (existsSync(cachePath)) {\n markCacheEntryUsed(cachePath);\n maintainProxyCache(dirname(cachePath));\n return cachePath;\n }\n\n const rememberedFailure = failedTranscodes.get(cachePath);\n if (rememberedFailure) {\n if (rememberedFailure.expiresAt > Date.now()) throw rememberedFailure.error;\n failedTranscodes.delete(cachePath);\n }\n\n const existing = inFlight.get(cachePath);\n if (existing) return existing;\n\n const promise = transcodeToCache(source.projectDir, source.sourcePath, cachePath, variant, box)\n .catch((err: unknown) => {\n if (!(err instanceof ProxyCapacityError)) rememberFailure(cachePath, err);\n throw err;\n })\n .finally(() => {\n inFlight.delete(cachePath);\n settledProxyCount += 1;\n });\n inFlight.set(cachePath, promise);\n return promise;\n}\n","import { existsSync, readdirSync, statSync, unlinkSync } from \"node:fs\";\nimport { extname, join } from \"node:path\";\nimport { PROXY_VARIANT_CONFIG } from \"./mediaCodecMap.js\";\n\nconst DEFAULT_MAX_BYTES = 10 * 1024 * 1024 * 1024;\nconst DEFAULT_STALE_TEMP_MS = 60 * 60 * 1000;\nconst DEFAULT_MIN_SWEEP_INTERVAL_MS = 5 * 60 * 1000;\nconst PROXY_EXTENSIONS: ReadonlySet<string> = new Set(\n Object.values(PROXY_VARIANT_CONFIG).map(({ extension }) => extension),\n);\nconst TRANSCODER_PROXY_NAME = /^[0-9a-f]{64}\\.\\w+$/;\nconst TRANSCODER_TEMP_NAME = /^\\.tmp-[0-9a-f-]{36}-[0-9a-f]{64}\\.\\w+$/;\n\nexport interface ProxyCacheCleanupOptions {\n maxBytes?: number;\n maxIdleMs?: number;\n staleTempMs?: number;\n minSweepIntervalMs?: number;\n protectedPaths?: ReadonlySet<string>;\n now?: number;\n dryRun?: boolean;\n}\n\nexport interface ProxyCacheCleanupResult {\n removed: string[];\n bytesBefore: number;\n bytesAfter: number;\n skipped: boolean;\n}\n\ninterface CacheEntry {\n path: string;\n size: number;\n modifiedAt: number;\n protected: boolean;\n}\n\nconst lastSweepAt = new Map<string, number>();\n\nfunction positiveEnvNumber(name: string, fallback: number): number {\n const parsed = Number(process.env[name]);\n return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;\n}\n\nfunction proxyCacheCleanupDefaults(): Required<\n Pick<ProxyCacheCleanupOptions, \"maxBytes\" | \"maxIdleMs\" | \"staleTempMs\" | \"minSweepIntervalMs\">\n> {\n return {\n maxBytes: positiveEnvNumber(\"HYPERFRAMES_PROXY_CACHE_MAX_BYTES\", DEFAULT_MAX_BYTES),\n maxIdleMs: positiveEnvNumber(\"HYPERFRAMES_PROXY_CACHE_MAX_IDLE_DAYS\", 30) * 24 * 60 * 60 * 1000,\n staleTempMs: positiveEnvNumber(\"HYPERFRAMES_PROXY_CACHE_STALE_TEMP_MS\", DEFAULT_STALE_TEMP_MS),\n minSweepIntervalMs: positiveEnvNumber(\n \"HYPERFRAMES_PROXY_CACHE_SWEEP_INTERVAL_MS\",\n DEFAULT_MIN_SWEEP_INTERVAL_MS,\n ),\n };\n}\n\nfunction shouldSkipSweep(cacheDir: string, now: number, minSweepIntervalMs: number): boolean {\n const previousSweep = lastSweepAt.get(cacheDir);\n if (previousSweep !== undefined && now - previousSweep < minSweepIntervalMs) return true;\n lastSweepAt.set(cacheDir, now);\n return false;\n}\n\nfunction readCacheInventory(\n cacheDir: string,\n protectedPaths: ReadonlySet<string>,\n now: number,\n staleTempMs: number,\n): { entries: CacheEntry[]; staleTemps: CacheEntry[] } {\n const entries: CacheEntry[] = [];\n const staleTemps: CacheEntry[] = [];\n for (const dirent of readdirSync(cacheDir, { withFileTypes: true })) {\n if (!dirent.isFile()) continue;\n const path = join(cacheDir, dirent.name);\n const stat = statSync(path);\n const entry = {\n path,\n size: stat.size,\n modifiedAt: stat.mtimeMs,\n protected: protectedPaths.has(path),\n };\n if (TRANSCODER_TEMP_NAME.test(dirent.name)) {\n if (now - stat.mtimeMs >= staleTempMs) staleTemps.push(entry);\n } else if (\n TRANSCODER_PROXY_NAME.test(dirent.name) &&\n PROXY_EXTENSIONS.has(extname(dirent.name))\n ) {\n entries.push(entry);\n }\n }\n const oldestFirst = (a: CacheEntry, b: CacheEntry): number =>\n a.modifiedAt - b.modifiedAt || a.path.localeCompare(b.path);\n entries.sort(oldestFirst);\n staleTemps.sort(oldestFirst);\n return { entries, staleTemps };\n}\n\nfunction evictCacheEntries(\n entries: CacheEntry[],\n staleTemps: CacheEntry[],\n now: number,\n maxIdleMs: number,\n maxBytes: number,\n dryRun: boolean,\n): Omit<ProxyCacheCleanupResult, \"skipped\"> {\n const bytesBefore = entries.reduce((total, entry) => total + entry.size, 0);\n let bytesAfter = bytesBefore;\n const removed: string[] = [];\n const remove = (entry: CacheEntry, countsTowardBudget: boolean): void => {\n if (!dryRun) unlinkSync(entry.path);\n removed.push(entry.path);\n if (countsTowardBudget) bytesAfter -= entry.size;\n };\n\n for (const entry of staleTemps) remove(entry, false);\n const kept: CacheEntry[] = [];\n for (const entry of entries) {\n if (!entry.protected && now - entry.modifiedAt >= maxIdleMs) remove(entry, true);\n else kept.push(entry);\n }\n for (const entry of kept) {\n if (bytesAfter <= maxBytes) break;\n if (!entry.protected && existsSync(entry.path)) remove(entry, true);\n }\n return { removed, bytesBefore, bytesAfter };\n}\n\n/**\n * Opportunistically bounds a project's transparent-proxy cache. Cleanup is\n * synchronous because callers already perform filesystem bookkeeping on the\n * preview request path, but rate limiting keeps the directory scan off the\n * hot path. Errors intentionally bubble so callers can warn without turning\n * a cache-maintenance failure into a preview failure.\n */\nexport function cleanupProxyCache(\n cacheDir: string,\n options: ProxyCacheCleanupOptions = {},\n): ProxyCacheCleanupResult {\n const defaults = proxyCacheCleanupDefaults();\n const now = options.now ?? Date.now();\n const minSweepIntervalMs = options.minSweepIntervalMs ?? defaults.minSweepIntervalMs;\n if (shouldSkipSweep(cacheDir, now, minSweepIntervalMs)) {\n return { removed: [], bytesBefore: 0, bytesAfter: 0, skipped: true };\n }\n if (!existsSync(cacheDir)) {\n return { removed: [], bytesBefore: 0, bytesAfter: 0, skipped: false };\n }\n\n const maxBytes = options.maxBytes ?? defaults.maxBytes;\n const maxIdleMs = options.maxIdleMs ?? defaults.maxIdleMs;\n const staleTempMs = options.staleTempMs ?? defaults.staleTempMs;\n const protectedPaths = options.protectedPaths ?? new Set<string>();\n const { entries, staleTemps } = readCacheInventory(cacheDir, protectedPaths, now, staleTempMs);\n return {\n ...evictCacheEntries(entries, staleTemps, now, maxIdleMs, maxBytes, options.dryRun ?? false),\n skipped: false,\n };\n}\n"],"mappings":";;;;;;;;;;;;AAAA,SAAS,aAAa;AACtB,SAAS,YAAY,kBAAkB;AACvC,SAAS,cAAAA,aAAY,YAAY,YAAAC,WAAU,cAAAC,aAAY,kBAAkB;AACzE,SAAS,UAAU,SAAS,YAAY,QAAAC,OAAM,UAAU,SAAS,WAAW;AAC5E;AAAA,EACE;AAAA,EACA;AAAA,OAEK;AACP,SAAS,oBAAoB;;;ACT7B,SAAS,YAAY,aAAa,UAAU,kBAAkB;AAC9D,SAAS,SAAS,YAAY;AAG9B,IAAM,oBAAoB,KAAK,OAAO,OAAO;AAC7C,IAAM,wBAAwB,KAAK,KAAK;AACxC,IAAM,gCAAgC,IAAI,KAAK;AAC/C,IAAM,mBAAwC,IAAI;AAAA,EAChD,OAAO,OAAO,oBAAoB,EAAE,IAAI,CAAC,EAAE,UAAU,MAAM,SAAS;AACtE;AACA,IAAM,wBAAwB;AAC9B,IAAM,uBAAuB;AA0B7B,IAAM,cAAc,oBAAI,IAAoB;AAE5C,SAAS,kBAAkB,MAAc,UAA0B;AACjE,QAAM,SAAS,OAAO,QAAQ,IAAI,IAAI,CAAC;AACvC,SAAO,OAAO,SAAS,MAAM,KAAK,SAAS,IAAI,SAAS;AAC1D;AAEA,SAAS,4BAEP;AACA,SAAO;AAAA,IACL,UAAU,kBAAkB,qCAAqC,iBAAiB;AAAA,IAClF,WAAW,kBAAkB,yCAAyC,EAAE,IAAI,KAAK,KAAK,KAAK;AAAA,IAC3F,aAAa,kBAAkB,yCAAyC,qBAAqB;AAAA,IAC7F,oBAAoB;AAAA,MAClB;AAAA,MACA;AAAA,IACF;AAAA,EACF;AACF;AAEA,SAAS,gBAAgB,UAAkB,KAAa,oBAAqC;AAC3F,QAAM,gBAAgB,YAAY,IAAI,QAAQ;AAC9C,MAAI,kBAAkB,UAAa,MAAM,gBAAgB,mBAAoB,QAAO;AACpF,cAAY,IAAI,UAAU,GAAG;AAC7B,SAAO;AACT;AAEA,SAAS,mBACP,UACA,gBACA,KACA,aACqD;AACrD,QAAM,UAAwB,CAAC;AAC/B,QAAM,aAA2B,CAAC;AAClC,aAAW,UAAU,YAAY,UAAU,EAAE,eAAe,KAAK,CAAC,GAAG;AACnE,QAAI,CAAC,OAAO,OAAO,EAAG;AACtB,UAAM,OAAO,KAAK,UAAU,OAAO,IAAI;AACvC,UAAM,OAAO,SAAS,IAAI;AAC1B,UAAM,QAAQ;AAAA,MACZ;AAAA,MACA,MAAM,KAAK;AAAA,MACX,YAAY,KAAK;AAAA,MACjB,WAAW,eAAe,IAAI,IAAI;AAAA,IACpC;AACA,QAAI,qBAAqB,KAAK,OAAO,IAAI,GAAG;AAC1C,UAAI,MAAM,KAAK,WAAW,YAAa,YAAW,KAAK,KAAK;AAAA,IAC9D,WACE,sBAAsB,KAAK,OAAO,IAAI,KACtC,iBAAiB,IAAI,QAAQ,OAAO,IAAI,CAAC,GACzC;AACA,cAAQ,KAAK,KAAK;AAAA,IACpB;AAAA,EACF;AACA,QAAM,cAAc,CAAC,GAAe,MAClC,EAAE,aAAa,EAAE,cAAc,EAAE,KAAK,cAAc,EAAE,IAAI;AAC5D,UAAQ,KAAK,WAAW;AACxB,aAAW,KAAK,WAAW;AAC3B,SAAO,EAAE,SAAS,WAAW;AAC/B;AAEA,SAAS,kBACP,SACA,YACA,KACA,WACA,UACA,QAC0C;AAC1C,QAAM,cAAc,QAAQ,OAAO,CAAC,OAAO,UAAU,QAAQ,MAAM,MAAM,CAAC;AAC1E,MAAI,aAAa;AACjB,QAAM,UAAoB,CAAC;AAC3B,QAAM,SAAS,CAAC,OAAmB,uBAAsC;AACvE,QAAI,CAAC,OAAQ,YAAW,MAAM,IAAI;AAClC,YAAQ,KAAK,MAAM,IAAI;AACvB,QAAI,mBAAoB,eAAc,MAAM;AAAA,EAC9C;AAEA,aAAW,SAAS,WAAY,QAAO,OAAO,KAAK;AACnD,QAAM,OAAqB,CAAC;AAC5B,aAAW,SAAS,SAAS;AAC3B,QAAI,CAAC,MAAM,aAAa,MAAM,MAAM,cAAc,UAAW,QAAO,OAAO,IAAI;AAAA,QAC1E,MAAK,KAAK,KAAK;AAAA,EACtB;AACA,aAAW,SAAS,MAAM;AACxB,QAAI,cAAc,SAAU;AAC5B,QAAI,CAAC,MAAM,aAAa,WAAW,MAAM,IAAI,EAAG,QAAO,OAAO,IAAI;AAAA,EACpE;AACA,SAAO,EAAE,SAAS,aAAa,WAAW;AAC5C;AASO,SAAS,kBACd,UACA,UAAoC,CAAC,GACZ;AACzB,QAAM,WAAW,0BAA0B;AAC3C,QAAM,MAAM,QAAQ,OAAO,KAAK,IAAI;AACpC,QAAM,qBAAqB,QAAQ,sBAAsB,SAAS;AAClE,MAAI,gBAAgB,UAAU,KAAK,kBAAkB,GAAG;AACtD,WAAO,EAAE,SAAS,CAAC,GAAG,aAAa,GAAG,YAAY,GAAG,SAAS,KAAK;AAAA,EACrE;AACA,MAAI,CAAC,WAAW,QAAQ,GAAG;AACzB,WAAO,EAAE,SAAS,CAAC,GAAG,aAAa,GAAG,YAAY,GAAG,SAAS,MAAM;AAAA,EACtE;AAEA,QAAM,WAAW,QAAQ,YAAY,SAAS;AAC9C,QAAM,YAAY,QAAQ,aAAa,SAAS;AAChD,QAAM,cAAc,QAAQ,eAAe,SAAS;AACpD,QAAM,iBAAiB,QAAQ,kBAAkB,oBAAI,IAAY;AACjE,QAAM,EAAE,SAAS,WAAW,IAAI,mBAAmB,UAAU,gBAAgB,KAAK,WAAW;AAC7F,SAAO;AAAA,IACL,GAAG,kBAAkB,SAAS,YAAY,KAAK,WAAW,UAAU,QAAQ,UAAU,KAAK;AAAA,IAC3F,SAAS;AAAA,EACX;AACF;;;AD/HO,IAAM,uBAAuB;AAE7B,IAAM,iBAAiB;AAE9B,SAAS,kBAAkB,MAAc,UAAkB,KAAa,KAAqB;AAC3F,QAAM,MAAM,QAAQ,IAAI,IAAI,GAAG,KAAK;AACpC,MAAI,CAAC,IAAK,QAAO;AACjB,QAAM,SAAS,OAAO,GAAG;AACzB,SAAO,OAAO,cAAc,MAAM,KAAK,UAAU,OAAO,UAAU,MAAM,SAAS;AACnF;AAKA,IAAM,4BAA4B,kBAAkB,qCAAqC,GAAG,GAAG,EAAE;AACjG,IAAM,wBAAwB,kBAAkB,+BAA+B,GAAG,GAAG,GAAG;AAExF,IAAM,wBAAwB;AACvB,IAAM,uBAAuB,KAAK,KAAK;AAC9C,IAAM,uBAAuB,KAAK;AAC3B,IAAM,oCAAoC;AACjD,IAAM,6BAA6B,IAAI,oCAAoC;AAC3E,IAAM,4BAA4B;AAC3B,IAAM,gCAAgC,IAAI,KAAK;AAE/C,IAAM,sBAAN,cAAkC,MAAM;AAAA,EACpC;AAAA,EACA;AAAA,EAET,YAAY,SAAiB,UAAyB,YAAoB;AACxE,UAAM,OAAO;AACb,SAAK,OAAO;AACZ,SAAK,WAAW;AAChB,SAAK,aAAa;AAAA,EACpB;AACF;AAKA,IAAM,yBAAN,cAAqC,oBAAoB;AAAA,EACvD,cAAc;AACZ,UAAM,2BAA2B,MAAM,EAAE;AAAA,EAC3C;AACF;AAEO,IAAM,2BAAN,cAAuC,oBAAoB;AAAA,EAChE,cAAc;AACZ;AAAA,MACE;AAAA,MACA;AAAA,MACA;AAAA,IACF;AACA,SAAK,OAAO;AAAA,EACd;AACF;AAEO,IAAM,qBAAN,cAAiC,oBAAoB;AAAA,EAC1D,cAAc;AACZ,UAAM,4CAA4C,MAAM,EAAE;AAC1D,SAAK,OAAO;AAAA,EACd;AACF;AAEO,IAAM,iCAAN,cAA6C,oBAAoB;AAAA,EACtE,cAAc;AACZ,UAAM,4DAA4D,MAAM,EAAE;AAC1E,SAAK,OAAO;AAAA,EACd;AACF;AAEO,IAAM,wBAAN,cAAoC,oBAAoB;AAAA,EAC7D,YAAY,WAAmB;AAC7B,UAAM,2CAA2C,SAAS,MAAM,MAAM,EAAE;AACxE,SAAK,OAAO;AAAA,EACd;AACF;AAIA,eAAsB,aACpB,SACA,YAAY,+BACA;AACZ,MAAI;AACJ,MAAI;AACF,WAAO,MAAM,QAAQ,KAAK;AAAA,MACxB;AAAA,MACA,IAAI,QAAe,CAAC,UAAU,WAAW;AACvC,gBAAQ,WAAW,MAAM,OAAO,IAAI,sBAAsB,SAAS,CAAC,GAAG,SAAS;AAChF,cAAM,MAAM;AAAA,MACd,CAAC;AAAA,IACH,CAAC;AAAA,EACH,UAAE;AACA,QAAI,MAAO,cAAa,KAAK;AAAA,EAC/B;AACF;AAiBA,SAAS,wBACP,YACA,oBACsB;AACtB,QAAM,sBAAsB,QAAQ,UAAU;AAC9C,QAAM,sBAAsB,QAAQ,kBAAkB;AACtD,QAAM,wBAAwB,SAAS,qBAAqB,mBAAmB;AAC/E,MACE,0BAA0B,QAC1B,sBAAsB,WAAW,KAAK,GAAG,EAAE,KAC3C,WAAW,qBAAqB,GAChC;AACA,UAAM,IAAI,+BAA+B;AAAA,EAC3C;AAEA,QAAM,sBAAsB,gBAAgB,UAAU;AACtD,QAAM,sBAAsB,SAAS,kBAAkB;AACvD,QAAM,wBAAwB,SAAS,qBAAqB,mBAAmB;AAC/E,QAAM,iCACJ,0BAA0B,QAC1B,CAAC,sBAAsB,WAAW,KAAK,GAAG,EAAE,KAC5C,CAAC,WAAW,qBAAqB;AACnC,SAAO;AAAA,IACL,YAAY;AAAA,IACZ,YAAY;AAAA,IACZ,cAAc,sBAAsB,UAAU,KAAK;AAAA;AAAA;AAAA,IAGnD,gBAAgB,iCACZ,wBACA,qBACF,UAAU,KAAK;AAAA,EACnB;AACF;AAEA,SAAS,mBACP,QACA,SACA,KACQ;AACR,QAAM,OAAOC,UAAS,OAAO,UAAU;AACvC,QAAM,OAAO,MAAM,KAAK,sBAAsB,GAAG,CAAC,KAAK;AACvD,SAAO,WAAW,QAAQ,EACvB;AAAA,IACC,GAAG,OAAO,YAAY,KAAK,OAAO,aAAa,KAAK,KAAK,OAAO,KAAK,KAAK,IAAI,KAAK,oBAAoB,KAAK,OAAO,GAAG,IAAI;AAAA,EAC5H,EACC,OAAO,KAAK;AACjB;AAEA,SAAS,2BACP,QACA,SACA,KACQ;AACR,QAAM,MAAM,mBAAmB,QAAQ,SAAS,GAAG;AACnD,SAAOC;AAAA,IACL,OAAO;AAAA,IACP;AAAA,IACA,GAAG,GAAG,GAAG,qBAAqB,OAAO,EAAE,SAAS;AAAA,EAClD;AACF;AAOO,SAAS,kBACd,YACA,oBACA,UAAwB,QAChB;AACR,SAAO;AAAA,IACL,wBAAwB,YAAY,kBAAkB;AAAA,IACtD;AAAA,EACF;AACF;AAOA,IAAI,mBAAmB;AACvB,IAAM,YAA+B,CAAC;AAEtC,SAAS,cAA6B;AACpC,SAAO,IAAI,QAAQ,CAAC,aAAa,WAAW;AAC1C,UAAM,aAAa,MAAY;AAC7B,UAAI,mBAAmB,2BAA2B;AAChD;AACA,oBAAY;AAAA,MACd,OAAO;AACL,YAAI,UAAU,UAAU,uBAAuB;AAC7C,iBAAO,IAAI,mBAAmB,CAAC;AAC/B;AAAA,QACF;AACA,kBAAU,KAAK,UAAU;AAAA,MAC3B;AAAA,IACF;AACA,eAAW;AAAA,EACb,CAAC;AACH;AAEA,SAAS,cAAoB;AAC3B;AACA,QAAM,OAAO,UAAU,MAAM;AAC7B,MAAI,KAAM,MAAK;AACjB;AAIA,IAAM,WAAW,oBAAI,IAA6B;AAElD,SAAS,mBAAmB,UAAwB;AAClD,MAAI;AACF,sBAAkB,UAAU,EAAE,gBAAgB,IAAI,IAAI,SAAS,KAAK,CAAC,EAAE,CAAC;AAAA,EAC1E,SAAS,OAAO;AAEd,YAAQ;AAAA,MACN,uCAAuC,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,CAAC;AAAA,IAC/F;AAAA,EACF;AACF;AAEA,SAAS,mBAAmB,WAAyB;AACnD,MAAI;AACF,UAAM,MAAM,oBAAI,KAAK;AACrB,eAAW,WAAW,KAAK,GAAG;AAAA,EAChC,QAAQ;AAAA,EAGR;AACF;AAaA,IAAM,mBAAmB,oBAAI,IAA+B;AAE5D,IAAI;AAEJ,SAAS,iBAAiB,YAAmC;AAC3D,MAAI,gBAAgB,eAAe,WAAY,QAAO,eAAe;AACrE,QAAM,UAAU,IAAI,QAAc,CAAC,cAAc,gBAAgB;AAC/D,UAAM,OAAO,MAAM,YAAY,CAAC,gBAAgB,UAAU,GAAG;AAAA,MAC3D,OAAO,CAAC,UAAU,QAAQ,MAAM;AAAA,MAChC,aAAa;AAAA,IACf,CAAC;AACD,QAAI,SAAS;AACb,SAAK,QAAQ,GAAG,QAAQ,CAAC,UAAkB;AACzC,gBAAU,MAAM,SAAS;AAAA,IAC3B,CAAC;AACD,SAAK,GAAG,SAAS,MAAM,YAAY,IAAI,yBAAyB,CAAC,CAAC;AAClE,SAAK,GAAG,SAAS,CAAC,SAAS;AACzB,UAAI,SAAS,KAAK,CAAC,aAAa,KAAK,MAAM,KAAK,CAAC,cAAc,KAAK,MAAM,GAAG;AAC3E,oBAAY,IAAI,yBAAyB,CAAC;AAAA,MAC5C,OAAO;AACL,qBAAa;AAAA,MACf;AAAA,IACF,CAAC;AAAA,EACH,CAAC;AACD,mBAAiB,EAAE,YAAY,QAAQ;AACvC,SAAO;AACT;AAEA,SAAS,gBAAgB,WAAmB,OAAsB;AAChE,QAAM,QACJ,iBAAiB,0BAA0B,iBAAiB,2BACxD,6BACA;AACN,mBAAiB,OAAO,SAAS;AACjC,mBAAiB,IAAI,WAAW,EAAE,OAAO,WAAW,KAAK,IAAI,IAAI,MAAM,CAAC;AACxE,SAAO,iBAAiB,OAAO,2BAA2B;AACxD,UAAM,SAAS,iBAAiB,KAAK,EAAE,KAAK,EAAE;AAC9C,QAAI,WAAW,OAAW;AAC1B,qBAAiB,OAAO,MAAM;AAAA,EAChC;AACF;AAIO,SAAS,+BAAqC;AACnD,mBAAiB,MAAM;AACzB;AAGA,SAAS,iBAAiB,KAA+B;AACvD,MAAI,CAAC,IAAK,QAAO;AACjB,QAAM,SAAS,eAAe,IAAI,KAAK,SAAS,IAAI,MAAM;AAC1D,QAAM,OAAO,CAAC,MAAc,aAAa,CAAC,gBAAgB,CAAC,IAAI,MAAM;AACrE,SAAO,SAAS,KAAK,IAAI,CAAC,IAAI,KAAK,IAAI,CAAC;AAC1C;AAEA,eAAe,UACb,YACA,YACA,SACA,KACe;AACf,QAAM,WAAW,MAAM,mBAAmB,UAAU;AACpD,QAAM,aAAa,aAAa,UAAU,EAAE,qBAAqB,KAAK,CAAC;AACvE,MAAI,CAAC,YAAY;AACf,UAAM,IAAI,uBAAuB;AAAA,EACnC;AACA,QAAM,aAAa,YAAY;AAC/B,QAAM,EAAE,YAAY,IAAI,SAAS;AACjC,QAAM,WAAW,gBAAgB,QAAQ,gBAAgB,UAAU,CAAC;AACpE,MAAI,QAAS,OAAM,iBAAiB,UAAU;AAC9C,QAAM,aAAa,UAAU,MAAM,sBAAsB,UAAU,IAAI,CAAC;AACxE,QAAM,YAAY,iBAAiB,GAAG;AACtC,QAAM,cAAc,aAAa,aAAa;AAE9C,QAAM,cAAc,UAChB;AAAA,IACE,sBAAsB,SAAS,OAAO,UAAU;AAAA,IAChD,GAAG,SAAS;AAAA,IACZ,UAAU,WAAW;AAAA,EACvB,EAAE,KAAK,GAAG,IACV,CAAC,WAAW,UAAU,WAAW,EAAE,EAAE,KAAK,GAAG;AAEjD,SAAO,IAAI,QAAQ,CAAC,gBAAgB,WAAW;AAC7C,UAAM,aAAa,CAAC,MAAM,MAAM,YAAY,OAAO,WAAW;AAC9D,UAAM,WAAW;AAAA,MACf;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AACA,UAAM,UAAU;AAAA,MACd;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AACA,UAAM,OAAO,CAAC,GAAG,YAAY,GAAI,YAAY,QAAQ,UAAU,UAAW,UAAU;AAMpF,UAAM,OAAO,MAAM,YAAY,MAAM;AAAA,MACnC,OAAO,CAAC,UAAU,UAAU,MAAM;AAAA,MAClC,SAAS;AAAA,MACT,YAAY;AAAA,MACZ,aAAa;AAAA,IACf,CAAC;AACD,QAAI,aAAa;AACjB,SAAK,QAAQ,GAAG,QAAQ,CAAC,UAAkB;AACzC,oBAAc,aAAa,MAAM,SAAS,GAAG,MAAM,CAAC,qBAAqB;AAAA,IAC3E,CAAC;AACD,SAAK,GAAG,SAAS,CAAC,QAAQ;AACxB,aAAO,IAAI,oBAAoB,2BAA2B,IAAI,OAAO,IAAI,MAAM,UAAU,CAAC;AAAA,IAC5F,CAAC;AACD,SAAK,GAAG,SAAS,CAAC,MAAM,WAAW;AACjC,UAAI,SAAS,GAAG;AACd,uBAAe;AAAA,MACjB,WAAW,QAAQ;AACjB;AAAA,UACE,IAAI;AAAA,YACF,oBAAoB,MAAM,aAAa,oBAAoB;AAAA,YAC3D;AAAA,YACA;AAAA,UACF;AAAA,QACF;AAAA,MACF,OAAO;AACL,eAAO,IAAI,oBAAoB,2BAA2B,IAAI,IAAI,MAAM,UAAU,CAAC;AAAA,MACrF;AAAA,IACF,CAAC;AAAA,EACH,CAAC;AACH;AAEA,eAAe,iBACb,YACA,oBACA,WACA,SACA,KACiB;AACjB,QAAM,YAAY;AAClB,MAAI;AAEF,QAAIC,YAAW,SAAS,EAAG,QAAO;AAElC,UAAM,WAAW,QAAQ,SAAS;AAClC,uBAAmB,YAAY,QAAQ;AACvC,UAAM,WAAWD,MAAK,UAAU,QAAQ,WAAW,CAAC,IAAI,SAAS,SAAS,CAAC,EAAE;AAC7E,QAAI;AACF,YAAM,UAAU,oBAAoB,UAAU,SAAS,GAAG;AAC1D,iBAAW,UAAU,SAAS;AAC9B,yBAAmB,QAAQ;AAC3B,aAAO;AAAA,IACT,UAAE;AAGA,UAAIC,YAAW,QAAQ,EAAG,CAAAC,YAAW,QAAQ;AAAA,IAC/C;AAAA,EACF,UAAE;AACA,gBAAY;AAAA,EACd;AACF;AAEA,IAAI,oBAAoB;AAGjB,SAAS,kBAAkB,YAAmC;AACnE,MAAI,CAACD,YAAW,UAAU,EAAG,QAAO,OAAO,iBAAiB;AAC5D,QAAM,WAAWD,MAAK,SAAS,UAAU,GAAG,cAAc,IAAI;AAC9D,aAAW,aAAa,SAAS,KAAK,EAAG,KAAI,UAAU,WAAW,QAAQ,EAAG,QAAO;AACpF,SAAO,OAAO,iBAAiB;AACjC;AAYA,eAAsB,aACpB,YACA,oBACA,UAAwB,QACxB,KACiB;AACjB,QAAM,SAAS,wBAAwB,YAAY,kBAAkB;AACrE,QAAM,YAAY,2BAA2B,QAAQ,SAAS,GAAG;AACjE,MAAIC,YAAW,SAAS,GAAG;AACzB,uBAAmB,SAAS;AAC5B,uBAAmB,QAAQ,SAAS,CAAC;AACrC,WAAO;AAAA,EACT;AAEA,QAAM,oBAAoB,iBAAiB,IAAI,SAAS;AACxD,MAAI,mBAAmB;AACrB,QAAI,kBAAkB,YAAY,KAAK,IAAI,EAAG,OAAM,kBAAkB;AACtE,qBAAiB,OAAO,SAAS;AAAA,EACnC;AAEA,QAAM,WAAW,SAAS,IAAI,SAAS;AACvC,MAAI,SAAU,QAAO;AAErB,QAAM,UAAU,iBAAiB,OAAO,YAAY,OAAO,YAAY,WAAW,SAAS,GAAG,EAC3F,MAAM,CAAC,QAAiB;AACvB,QAAI,EAAE,eAAe,oBAAqB,iBAAgB,WAAW,GAAG;AACxE,UAAM;AAAA,EACR,CAAC,EACA,QAAQ,MAAM;AACb,aAAS,OAAO,SAAS;AACzB,yBAAqB;AAAA,EACvB,CAAC;AACH,WAAS,IAAI,WAAW,OAAO;AAC/B,SAAO;AACT;","names":["existsSync","statSync","unlinkSync","join","statSync","join","existsSync","unlinkSync"]}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
export { a as PreviewApiAdapter, P as PreviewDocumentStore, i as injectMediaCodecMap, f as injectMediaCodecMapIntoHtml, g as isAutoProxyEnabled, p as proxyEtagSalt, r as resolvePreviewMediaCodecProbeCache } from '../mediaProxyPreview-CCxjB3rf.js';
|
|
2
|
-
import './mediaCodecMap.js';
|
|
3
1
|
import '@hyperframes/core';
|
|
2
|
+
export { a as PreviewApiAdapter, P as PreviewDocumentStore, i as injectMediaCodecMap, f as injectMediaCodecMapIntoHtml, g as isAutoProxyEnabled, p as proxyEtagSalt, r as resolvePreviewMediaCodecProbeCache } from '../mediaProxyPreview-CJ4lbPcF.js';
|
|
3
|
+
import './mediaCodecMap.js';
|
|
4
4
|
import '@hyperframes/parsers';
|
|
5
5
|
import '@hyperframes/core/compiler';
|
|
6
6
|
import '../projectHistory-Cm8Mdmmv.js';
|
|
@@ -4,8 +4,8 @@ import {
|
|
|
4
4
|
isAutoProxyEnabled,
|
|
5
5
|
proxyEtagSalt,
|
|
6
6
|
resolvePreviewMediaCodecProbeCache
|
|
7
|
-
} from "../chunk-
|
|
8
|
-
import "../chunk-
|
|
7
|
+
} from "../chunk-5VCIZYUE.js";
|
|
8
|
+
import "../chunk-QP7LKFU2.js";
|
|
9
9
|
import "../chunk-74JVVCWL.js";
|
|
10
10
|
import "../chunk-DTKK7TKL.js";
|
|
11
11
|
import "../chunk-6XMC64FJ.js";
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { PreviewProxyBox } from '@hyperframes/core';
|
|
1
2
|
import { ProxyVariant } from './mediaCodecMap.js';
|
|
2
3
|
|
|
3
4
|
/**
|
|
@@ -59,7 +60,9 @@ declare function proxyActivityMark(projectDir: string): string | null;
|
|
|
59
60
|
* one promise; calls for different keys queue through the global concurrency
|
|
60
61
|
* limiter above. Throws `ProxyTranscodeError` on failure (missing ffmpeg or a
|
|
61
62
|
* nonzero exit) — callers (route handlers) decide how to surface that (502).
|
|
63
|
+
* A `box` makes a smaller preview-only copy with its own cache entry; without
|
|
64
|
+
* one the copy keeps the source size (CLI play, static servers, publish).
|
|
62
65
|
*/
|
|
63
|
-
declare function resolveProxy(projectDir: string, absoluteSourcePath: string, variant?: ProxyVariant): Promise<string>;
|
|
66
|
+
declare function resolveProxy(projectDir: string, absoluteSourcePath: string, variant?: ProxyVariant, box?: PreviewProxyBox): Promise<string>;
|
|
64
67
|
|
|
65
68
|
export { CACHE_DIR_NAME, DEFAULT_PROXY_WAIT_TIMEOUT_MS, FfmpegMissingFilterError, PROXY_PARAMS_VERSION, PROXY_PENDING_RETRY_AFTER_SECONDS, ProxyCapacityError, ProxySourceOutsideProjectError, ProxyTranscodeError, ProxyWaitTimeoutError, TRANSCODE_TIMEOUT_MS, clearFailedTranscodesForTest, getProxyCachePath, proxyActivityMark, resolveProxy, waitForProxy };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Hono } from 'hono';
|
|
2
|
-
import { S as StudioApiAdapter, P as PreviewDocumentStore, M as MediaProcessingJobState } from './mediaProxyPreview-
|
|
3
|
-
export { L as LintResult, a as PreviewApiAdapter, R as RenderJobState, b as ResolvedProject, c as StudioSelectionResponse, d as StudioSelectionSnapshot, e as StudioSelectionTextField } from './mediaProxyPreview-
|
|
2
|
+
import { S as StudioApiAdapter, P as PreviewDocumentStore, M as MediaProcessingJobState } from './mediaProxyPreview-CJ4lbPcF.js';
|
|
3
|
+
export { L as LintResult, a as PreviewApiAdapter, R as RenderJobState, b as ResolvedProject, c as StudioSelectionResponse, d as StudioSelectionSnapshot, e as StudioSelectionTextField } from './mediaProxyPreview-CJ4lbPcF.js';
|
|
4
4
|
export { C as ClosedWindow, D as DEFAULT_HISTORY_ROOT, S as HISTORY_START, H as HistoryClosedError, a as HistoryEntry, b as HistoryEntrySide, c as HistoryFileChange, d as HistoryListItem, e as HistoryResult, f as HistoryWho, g as HistoryWindow, M as MAX_WINDOW_IDLE_MS, P as ProjectHistory, h as ProjectHistoryOptions, U as UNDO_MODES, i as UndoMode, o as openProjectHistory } from './projectHistory-Cm8Mdmmv.js';
|
|
5
5
|
export { HistoryBusyError, HistoryIdError, ProjectHistoryRecord, PrunedHistory, historyCache, listProjectHistories, pruneGoneProjectHistories } from './history/index.js';
|
|
6
6
|
export { CACHE_DIR_NAME as PROXY_CACHE_DIR_NAME } from './helpers/proxyTranscoder.js';
|
package/dist/index.js
CHANGED
|
@@ -64,7 +64,7 @@ import {
|
|
|
64
64
|
isAutoProxyEnabled,
|
|
65
65
|
proxyEtagSalt,
|
|
66
66
|
resolvePreviewMediaCodecProbeCache
|
|
67
|
-
} from "./chunk-
|
|
67
|
+
} from "./chunk-5VCIZYUE.js";
|
|
68
68
|
import {
|
|
69
69
|
CACHE_DIR_NAME,
|
|
70
70
|
PROXY_PENDING_RETRY_AFTER_SECONDS,
|
|
@@ -75,7 +75,7 @@ import {
|
|
|
75
75
|
proxyActivityMark,
|
|
76
76
|
resolveProxy,
|
|
77
77
|
waitForProxy
|
|
78
|
-
} from "./chunk-
|
|
78
|
+
} from "./chunk-QP7LKFU2.js";
|
|
79
79
|
import {
|
|
80
80
|
PROXY_VARIANT_CONFIG,
|
|
81
81
|
decideMediaProxyEligibility,
|
|
@@ -3615,6 +3615,7 @@ import {
|
|
|
3615
3615
|
STUDIO_PREVIEW_MARK_META,
|
|
3616
3616
|
STUDIO_PREVIEW_ERRORS
|
|
3617
3617
|
} from "@hyperframes/core/studio-preview-mark";
|
|
3618
|
+
import { PREVIEW_PROXY_BOX_PARAM, parsePreviewProxyBox } from "@hyperframes/core";
|
|
3618
3619
|
import { gsapCdnDist as gsapCdnDist2, motionPathPluginUrl } from "@hyperframes/core/gsap-cdn";
|
|
3619
3620
|
import { findStartTags as findStartTags2, injectTagsAtHeadStart } from "@hyperframes/core/compiler/html-document";
|
|
3620
3621
|
import { isWithinProjectRoot } from "@hyperframes/parsers/asset-resolution";
|
|
@@ -4189,6 +4190,11 @@ ${etag}`;
|
|
|
4189
4190
|
const contentType = getMimeType(subPath);
|
|
4190
4191
|
const isText = /\.(html|css|js|json|svg|txt|md|cube)$/i.test(subPath);
|
|
4191
4192
|
const proxyParam = c.req.query("hf-proxy");
|
|
4193
|
+
const boxParam = c.req.query(PREVIEW_PROXY_BOX_PARAM);
|
|
4194
|
+
const proxyBox = boxParam === void 0 ? void 0 : parsePreviewProxyBox(boxParam);
|
|
4195
|
+
if (proxyBox === null || proxyBox && proxyParam === void 0) {
|
|
4196
|
+
return c.text("not found", 404);
|
|
4197
|
+
}
|
|
4192
4198
|
let proxyVariant;
|
|
4193
4199
|
if (proxyParam !== void 0) {
|
|
4194
4200
|
if (!isProxyVariantRequest(proxyParam) || !contentType.startsWith("video/") || !isAutoProxyEnabled(adapter)) {
|
|
@@ -4206,7 +4212,7 @@ ${etag}`;
|
|
|
4206
4212
|
}
|
|
4207
4213
|
}
|
|
4208
4214
|
const tag = settledFileTag(stat2);
|
|
4209
|
-
const etag = tag && `"${tag}${proxyEtagSalt(proxyVariant)}"`;
|
|
4215
|
+
const etag = tag && `"${tag}${proxyEtagSalt(proxyVariant, proxyBox)}"`;
|
|
4210
4216
|
const cacheHeaders = isText ? { "Cache-Control": "no-store" } : { "Cache-Control": "private, no-cache", ...etag && { ETag: etag } };
|
|
4211
4217
|
if (!isText && etag) {
|
|
4212
4218
|
const ifNoneMatch = c.req.header("If-None-Match");
|
|
@@ -4218,7 +4224,7 @@ ${etag}`;
|
|
|
4218
4224
|
let servedContentType = contentType;
|
|
4219
4225
|
if (proxyVariant !== void 0) {
|
|
4220
4226
|
try {
|
|
4221
|
-
servedPath = await waitForProxy(resolveProxy(project.dir, file, proxyVariant), 0);
|
|
4227
|
+
servedPath = await waitForProxy(resolveProxy(project.dir, file, proxyVariant, proxyBox), 0);
|
|
4222
4228
|
} catch (err) {
|
|
4223
4229
|
if (err instanceof ProxyWaitTimeoutError) {
|
|
4224
4230
|
return c.text("media proxy is being made", 202, {
|