@hyperframes/studio-server 0.7.70 → 0.7.71

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperframes/studio-server",
3
- "version": "0.7.70",
3
+ "version": "0.7.71",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/heygen-com/hyperframes",
@@ -64,8 +64,8 @@
64
64
  "linkedom": "^0.18.12",
65
65
  "postcss": "^8.5.8",
66
66
  "postcss-selector-parser": "^7.1.2",
67
- "@hyperframes/core": "0.7.70",
68
- "@hyperframes/parsers": "0.7.70"
67
+ "@hyperframes/parsers": "0.7.71",
68
+ "@hyperframes/core": "0.7.71"
69
69
  },
70
70
  "devDependencies": {
71
71
  "@types/node": "^25.0.10",
@@ -1 +0,0 @@
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 {\n existsSync,\n mkdirSync,\n realpathSync,\n renameSync,\n statSync,\n unlinkSync,\n utimesSync,\n} from \"node:fs\";\nimport { basename, dirname, isAbsolute, join, relative, sep } from \"node:path\";\nimport { findFfBinary } from \"@hyperframes/parsers/ff-binaries\";\nimport { probeMediaMetadata } from \"./mediaMetadata.js\";\nimport { cleanupProxyCache } from \"./proxyCache.js\";\nimport { PROXY_VARIANT_CONFIG, type ProxyVariant } from \"./mediaCodecMap.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 = \"v4\";\n\nconst 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;\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 it is deliberately NOT remembered by the negative cache below\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 inside 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: source path relative to the project (so the\n * cache is portable across checkouts at different absolute locations), mtime\n * and file size (mtime alone can collide on same-second re-exports on\n * coarse-timestamp filesystems; size catches nearly all such cases at zero\n * cost), and a params version token so changing the ffmpeg recipe below\n * invalidates every cached proxy cleanly.\n */\ntype CanonicalProxySource = {\n projectDir: string;\n sourcePath: string;\n relativePath: string;\n};\n\nfunction canonicalizeProxySource(\n projectDir: string,\n absoluteSourcePath: string,\n): CanonicalProxySource {\n const canonicalProjectDir = realpathSync(projectDir);\n const canonicalSourcePath = realpathSync(absoluteSourcePath);\n const relPath = relative(canonicalProjectDir, canonicalSourcePath);\n if (relPath === \"..\" || relPath.startsWith(`..${sep}`) || isAbsolute(relPath)) {\n throw new ProxySourceOutsideProjectError();\n }\n return {\n projectDir: canonicalProjectDir,\n sourcePath: canonicalSourcePath,\n relativePath: relPath.normalize(\"NFC\"),\n };\n}\n\nfunction buildProxyCacheKey(source: CanonicalProxySource, variant: ProxyVariant): string {\n const stat = statSync(source.sourcePath);\n return createHash(\"sha256\")\n .update(\n `${source.relativePath}\\0${stat.mtimeMs}\\0${stat.size}\\0${PROXY_PARAMS_VERSION}\\0${variant}`,\n )\n .digest(\"hex\");\n}\n\nfunction getCanonicalProxyCachePath(source: CanonicalProxySource, variant: ProxyVariant): string {\n const key = buildProxyCacheKey(source, variant);\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: ProxyTranscodeError;\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 });\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: ProxyTranscodeError): void {\n failedTranscodes.delete(cachePath);\n failedTranscodes.set(cachePath, { error, expiresAt: Date.now() + FAILURE_CACHE_TTL_MS });\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\nasync function runFfmpeg(\n sourcePath: string,\n outputPath: string,\n variant: ProxyVariant,\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 // The HDR tonemap filters discard alpha. VP8 is the alpha-preserving proxy\n // variant, so retain its source color values instead of making it opaque.\n if (metadata.color.isHdr && variant !== \"vp8\") await ensureHdrFilters(ffmpegPath);\n const evenScale = \"scale=trunc(iw/2)*2:trunc(ih/2)*2\";\n const pixelFormat = variant === \"vp8\" ? \"yuva420p\" : \"yuv420p\";\n const videoFilter =\n metadata.color.isHdr && variant !== \"vp8\"\n ? [\n \"zscale=t=linear:npl=100\",\n \"tonemap=hable:desat=0\",\n \"zscale=p=bt709:t=bt709:m=bt709:r=tv\",\n evenScale,\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 });\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 absoluteSourcePath: string,\n cachePath: string,\n variant: ProxyVariant,\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 mkdirSync(cacheDir, { recursive: true });\n const tempPath = join(cacheDir, `.tmp-${randomUUID()}-${basename(cachePath)}`);\n try {\n await runFfmpeg(absoluteSourcePath, tempPath, variant);\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\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 */\nexport async function resolveProxy(\n projectDir: string,\n absoluteSourcePath: string,\n variant: ProxyVariant = \"h264\",\n): Promise<string> {\n const source = canonicalizeProxySource(projectDir, absoluteSourcePath);\n const cachePath = getCanonicalProxyCachePath(source, variant);\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.sourcePath, cachePath, variant)\n .catch((err: unknown) => {\n if (\n err instanceof ProxyTranscodeError &&\n !(err instanceof FfmpegUnavailableError) &&\n !(err instanceof FfmpegMissingFilterError) &&\n !(err instanceof ProxyCapacityError) &&\n !(err instanceof ProxySourceOutsideProjectError)\n ) {\n rememberFailure(cachePath, err);\n }\n throw err;\n })\n .finally(() => {\n inFlight.delete(cachePath);\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);\n\nexport interface ProxyCacheCleanupOptions {\n maxBytes?: number;\n maxIdleMs?: number;\n staleTempMs?: number;\n minSweepIntervalMs?: number;\n protectedPaths?: ReadonlySet<string>;\n now?: number;\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 (dirent.name.startsWith(\".tmp-\")) {\n if (now - stat.mtimeMs >= staleTempMs) staleTemps.push(entry);\n } else if (PROXY_EXTENSIONS.has(extname(dirent.name))) {\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): 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 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 for (const entry of entries) {\n if (!entry.protected && now - entry.modifiedAt >= maxIdleMs) remove(entry, true);\n }\n for (const entry of entries) {\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),\n skipped: false,\n };\n}\n"],"mappings":"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","names":["existsSync","statSync","unlinkSync","join","statSync","join","existsSync","unlinkSync"]}