hyperframes 0.7.94 → 0.7.96

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/cli.js CHANGED
@@ -50,7 +50,7 @@ var VERSION;
50
50
  var init_version = __esm({
51
51
  "src/version.ts"() {
52
52
  "use strict";
53
- VERSION = true ? "0.7.94" : "0.0.0-dev";
53
+ VERSION = true ? "0.7.96" : "0.0.0-dev";
54
54
  }
55
55
  });
56
56
 
@@ -479,6 +479,7 @@ var init_dist = __esm({
479
479
  });
480
480
 
481
481
  // src/telemetry/runId.ts
482
+ import { randomUUID } from "crypto";
482
483
  function getRunId() {
483
484
  if (!resolved) {
484
485
  const value = process.env["HYPERFRAMES_RUN_ID"]?.trim().slice(0, 128);
@@ -487,7 +488,11 @@ function getRunId() {
487
488
  }
488
489
  return runId;
489
490
  }
490
- var resolved, runId;
491
+ function getInvocationId() {
492
+ invocationId ??= randomUUID();
493
+ return invocationId;
494
+ }
495
+ var resolved, runId, invocationId;
491
496
  var init_runId = __esm({
492
497
  "src/telemetry/runId.ts"() {
493
498
  "use strict";
@@ -62374,10 +62379,15 @@ function parseCanaryOverride(raw) {
62374
62379
  function canaryFeatureKey(name) {
62375
62380
  return `${CANARY_FEATURE_PREFIX}${name}`;
62376
62381
  }
62382
+ function canaryReasonKey(name) {
62383
+ return `canary_reason_${name.replace(/[^A-Za-z0-9]+/g, "_")}`;
62384
+ }
62377
62385
  function canaryFeatureProperties(entries2) {
62378
62386
  const props = {};
62379
62387
  for (const entry of entries2) {
62380
62388
  props[canaryFeatureKey(entry.name)] = entry.enabled ? "true" : "false";
62389
+ if (entry.reason !== void 0)
62390
+ props[canaryReasonKey(entry.name)] = entry.reason;
62381
62391
  }
62382
62392
  return props;
62383
62393
  }
@@ -62735,7 +62745,7 @@ var init_policy = __esm({
62735
62745
  import { existsSync as existsSync2, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "fs";
62736
62746
  import { join as join3 } from "path";
62737
62747
  import { homedir } from "os";
62738
- import { randomUUID } from "crypto";
62748
+ import { randomUUID as randomUUID2 } from "crypto";
62739
62749
  function salvageInstallState(parsed) {
62740
62750
  const markerAt = typeof parsed.markerAt === "string" ? parsed.markerAt : void 0;
62741
62751
  const bucketSeed = parseNonEmptyString(parsed.bucketSeed);
@@ -62765,9 +62775,10 @@ function warnSeedBackfillFailed(error) {
62765
62775
  }
62766
62776
  function backfillBucketSeed(config) {
62767
62777
  const recorded = readInstallState();
62768
- config.bucketSeed = (isInstallState(recorded) ? recorded.bucketSeed : void 0) ?? randomUUID();
62778
+ config.bucketSeed = (isInstallState(recorded) ? recorded.bucketSeed : void 0) ?? randomUUID2();
62769
62779
  const write = writeConfigWithResult(config);
62770
62780
  if (!write.ok) warnSeedBackfillFailed(write.error);
62781
+ return write;
62771
62782
  }
62772
62783
  function installStateLatchedFired() {
62773
62784
  if (latchedFiredSeen) return true;
@@ -62856,6 +62867,7 @@ function mintAndCacheConfig() {
62856
62867
  const config = mintConfig();
62857
62868
  const write = writeConfigWithResult(config);
62858
62869
  if (!write.ok) warnSeedBackfillFailed(write.error);
62870
+ classifyIdentity(write.ok ? "unknown" : "process_only", writeOutcomeOf(write));
62859
62871
  cachedConfig = { ...config };
62860
62872
  return { ...config };
62861
62873
  }
@@ -62864,7 +62876,7 @@ function mintConfig() {
62864
62876
  const state = isInstallState(read) ? read : null;
62865
62877
  return {
62866
62878
  ...DEFAULT_CONFIG,
62867
- anonymousId: randomUUID(),
62879
+ anonymousId: randomUUID2(),
62868
62880
  // A corrupt record still means this machine had an install — reporting it
62869
62881
  // as `false` would count a partial disk write as a brand-new machine and
62870
62882
  // understate recoverable churn, which is the one thing this field exists
@@ -62875,7 +62887,7 @@ function mintConfig() {
62875
62887
  // machine stays tripped for the new one, and the canary bucketing seed is
62876
62888
  // inherited so cohorts hold across a config.json re-mint.
62877
62889
  deParallelRouterTrialFired: state?.deParallelRouterTrialFired === true ? true : void 0,
62878
- bucketSeed: state?.bucketSeed ?? randomUUID()
62890
+ bucketSeed: state?.bucketSeed ?? randomUUID2()
62879
62891
  };
62880
62892
  }
62881
62893
  function recordRecentRender(id, ok) {
@@ -62884,6 +62896,23 @@ function recordRecentRender(id, ok) {
62884
62896
  config.recentRenders = ring.slice(-MAX_RECENT_RENDERS);
62885
62897
  writeConfig(config);
62886
62898
  }
62899
+ function classifyIdentity(persistence, outcome) {
62900
+ if (identityPersistence !== void 0) return;
62901
+ identityPersistence = persistence;
62902
+ identityWriteOutcome = outcome;
62903
+ }
62904
+ function writeOutcomeOf(write) {
62905
+ if (!write.ok) return "failed";
62906
+ return write.mirrored === false ? "ok_unmirrored" : "ok";
62907
+ }
62908
+ function getIdentityPersistence() {
62909
+ readConfig();
62910
+ return identityPersistence ?? "unknown";
62911
+ }
62912
+ function getIdentityWriteOutcome() {
62913
+ readConfig();
62914
+ return identityWriteOutcome;
62915
+ }
62887
62916
  function parseNonEmptyString(value) {
62888
62917
  return typeof value === "string" && value.length > 0 ? value : void 0;
62889
62918
  }
@@ -62933,7 +62962,7 @@ function materializeConfig(parsed) {
62933
62962
  return {
62934
62963
  ...passthroughFields(parsed),
62935
62964
  telemetryEnabled: parsed.telemetryEnabled ?? DEFAULT_CONFIG.telemetryEnabled,
62936
- anonymousId: parsed.anonymousId || randomUUID(),
62965
+ anonymousId: parsed.anonymousId || randomUUID2(),
62937
62966
  telemetryNoticeShown: parsed.telemetryNoticeShown ?? DEFAULT_CONFIG.telemetryNoticeShown,
62938
62967
  commandCount: parsed.commandCount ?? DEFAULT_CONFIG.commandCount,
62939
62968
  renderSuccessCount: parsed.renderSuccessCount ?? DEFAULT_CONFIG.renderSuccessCount,
@@ -62949,16 +62978,21 @@ function readConfig() {
62949
62978
  const raw = readFileSync(CONFIG_FILE, "utf-8");
62950
62979
  const parsed = JSON.parse(raw);
62951
62980
  const config = materializeConfig(parsed);
62981
+ const idFromDisk = parseNonEmptyString(parsed.anonymousId) !== void 0;
62952
62982
  if (config.bucketSeed === void 0) {
62953
- backfillBucketSeed(config);
62983
+ const write = backfillBucketSeed(config);
62984
+ if (idFromDisk) classifyIdentity("durable");
62985
+ else classifyIdentity(write.ok ? "unknown" : "process_only", writeOutcomeOf(write));
62954
62986
  cachedConfig = config;
62955
62987
  return { ...config };
62956
62988
  }
62989
+ classifyIdentity(idFromDisk ? "durable" : "process_only");
62957
62990
  cachedConfig = config;
62958
62991
  return { ...config };
62959
62992
  } catch {
62960
62993
  const config = { ...mintConfig(), telemetryEnabled: false };
62961
- writeConfig(config);
62994
+ const write = writeConfigWithResult(config);
62995
+ classifyIdentity(write.ok ? "unknown" : "process_only", writeOutcomeOf(write));
62962
62996
  return config;
62963
62997
  }
62964
62998
  }
@@ -62988,7 +63022,7 @@ function incrementCommandCount() {
62988
63022
  writeConfig(config);
62989
63023
  return config.commandCount;
62990
63024
  }
62991
- var CONFIG_DIR, CONFIG_FILE, STATE_FILE, LEGACY_STATE_FILE, seedBackfillWarned, latchedFiredSeen, stateMarkerSynced, stateFiredSynced, MAX_RECENT_RENDERS, DEFAULT_CONFIG, cachedConfig, CONFIG_PATH, STATE_PATH;
63025
+ var CONFIG_DIR, CONFIG_FILE, STATE_FILE, LEGACY_STATE_FILE, seedBackfillWarned, latchedFiredSeen, stateMarkerSynced, stateFiredSynced, MAX_RECENT_RENDERS, DEFAULT_CONFIG, cachedConfig, identityPersistence, identityWriteOutcome, CONFIG_PATH, STATE_PATH;
62992
63026
  var init_config = __esm({
62993
63027
  "src/telemetry/config.ts"() {
62994
63028
  "use strict";
@@ -70836,7 +70870,7 @@ import {
70836
70870
  statSync as statSync4,
70837
70871
  unlinkSync
70838
70872
  } from "fs";
70839
- import { createHash, randomUUID as randomUUID2 } from "crypto";
70873
+ import { createHash, randomUUID as randomUUID3 } from "crypto";
70840
70874
  import { BlockList, isIP } from "net";
70841
70875
  import { dirname as dirname6, extname as extname2, join as join9 } from "path";
70842
70876
  import { Readable, Transform } from "stream";
@@ -71004,7 +71038,7 @@ async function acquireCachePathLock(localPath, timeoutMs, signal) {
71004
71038
  if (error.code !== "EEXIST") throw error;
71005
71039
  }
71006
71040
  if (createdLock) {
71007
- const owner = `${CACHE_LOCK_OWNER_PREFIX}${randomUUID2()}`;
71041
+ const owner = `${CACHE_LOCK_OWNER_PREFIX}${randomUUID3()}`;
71008
71042
  try {
71009
71043
  mkdirSync5(join9(lockPath, owner));
71010
71044
  const entries2 = readdirSync3(lockPath);
@@ -84955,7 +84989,7 @@ var init_htmlTemplate = __esm({
84955
84989
  });
84956
84990
 
84957
84991
  // ../engine/src/services/extractionCache.ts
84958
- import { createHash as createHash2, randomUUID as randomUUID3 } from "crypto";
84992
+ import { createHash as createHash2, randomUUID as randomUUID4 } from "crypto";
84959
84993
  import {
84960
84994
  existsSync as existsSync13,
84961
84995
  lstatSync as lstatSync2,
@@ -85003,7 +85037,7 @@ function lookupCacheEntry(rootDir, input2) {
85003
85037
  return { entry: { dir, keyHash }, hit: complete };
85004
85038
  }
85005
85039
  function partialCacheEntryDir(entry) {
85006
- return `${entry.dir}.partial-${process.pid}-${randomUUID3().slice(0, 8)}`;
85040
+ return `${entry.dir}.partial-${process.pid}-${randomUUID4().slice(0, 8)}`;
85007
85041
  }
85008
85042
  function isTargetExistsRenameError(err) {
85009
85043
  const code = err.code;
@@ -90728,7 +90762,10 @@ function canaryDecisionsForStudio() {
90728
90762
  }
90729
90763
  function canaryEventProperties() {
90730
90764
  return canaryFeatureProperties(
90731
- CANARIES.map((c3) => ({ name: c3.name, enabled: resolveCanary(c3.name).enabled }))
90765
+ CANARIES.map((c3) => {
90766
+ const { enabled, reason } = resolveCanary(c3.name);
90767
+ return { name: c3.name, enabled, reason };
90768
+ })
90732
90769
  );
90733
90770
  }
90734
90771
  var decisions, decisionsTelemetryPosture;
@@ -90746,10 +90783,10 @@ var init_canary2 = __esm({
90746
90783
 
90747
90784
  // src/telemetry/transport.ts
90748
90785
  import { spawn as spawn5 } from "child_process";
90749
- import { randomUUID as randomUUID4 } from "crypto";
90786
+ import { randomUUID as randomUUID5 } from "crypto";
90750
90787
  function enqueue(event, properties, distinctId) {
90751
90788
  eventQueue.push({
90752
- uuid: randomUUID4(),
90789
+ uuid: randomUUID5(),
90753
90790
  event,
90754
90791
  distinctId,
90755
90792
  properties,
@@ -90867,6 +90904,19 @@ function trackEvent(event, properties = {}, distinctId) {
90867
90904
  // could not read. Without it a partial disk write is indistinguishable
90868
90905
  // from a genuinely fresh install. Absent in the normal case.
90869
90906
  install_state_file_corrupt: readConfig().stateFileCorrupt,
90907
+ // Whether this process's anonymousId can be trusted to survive to the
90908
+ // next run: `durable` (loaded from a preexisting config), `unknown`
90909
+ // (minted+persisted this run — an ephemeral HOME is indistinguishable
90910
+ // from a genuine first run), `process_only` (persist failed). Install-
90911
+ // grain metrics should count only durable identities; the identity-
90912
+ // churn workloads (fresh id per run) are never durable.
90913
+ identity_persistence: getIdentityPersistence(),
90914
+ // Outcome of the identity-establishing config write; absent when the
90915
+ // identity came from disk and nothing needed writing.
90916
+ config_write_outcome: getIdentityWriteOutcome(),
90917
+ // Groups one invocation's events even when the install identity is
90918
+ // untrustworthy. Always present, unlike the orchestrator-set run_id.
90919
+ invocation_id: getInvocationId(),
90870
90920
  // Canary assignments as `$feature/canary-<name>` — PostHog's native flag
90871
90921
  // property shape, so breakdowns and experiment analysis work on a canary
90872
90922
  // with nothing configured server-side. On EVERY event, not just renders:
@@ -90901,6 +90951,7 @@ var init_client = __esm({
90901
90951
  "src/telemetry/client.ts"() {
90902
90952
  "use strict";
90903
90953
  init_config();
90954
+ init_runId();
90904
90955
  init_version();
90905
90956
  init_colors();
90906
90957
  init_diagnostics2();
@@ -94501,7 +94552,7 @@ import { execFileSync as execFileSync5 } from "child_process";
94501
94552
  import { existsSync as existsSync25, readFileSync as readFileSync14, mkdirSync as mkdirSync12, unlinkSync as unlinkSync3 } from "fs";
94502
94553
  import { join as join21, extname as extname5 } from "path";
94503
94554
  import { tmpdir as tmpdir3 } from "os";
94504
- import { randomUUID as randomUUID5 } from "crypto";
94555
+ import { randomUUID as randomUUID6 } from "crypto";
94505
94556
  function detectLanguage(whisperPath, modelPath2, wavPath) {
94506
94557
  try {
94507
94558
  const output = execFileSync5(whisperPath, ["--model", modelPath2, "--detect-language", wavPath], {
@@ -94643,7 +94694,7 @@ function isVideoFile(filePath) {
94643
94694
  return VIDEO_EXTENSIONS.has(extname5(filePath).toLowerCase());
94644
94695
  }
94645
94696
  function tempWavPath() {
94646
- return join21(tmpdir3(), `hyperframes-audio-${process.pid}-${randomUUID5()}.wav`);
94697
+ return join21(tmpdir3(), `hyperframes-audio-${process.pid}-${randomUUID6()}.wav`);
94647
94698
  }
94648
94699
  function extractAudio(videoPath) {
94649
94700
  const ffmpegPath = findFFmpeg();
@@ -99246,7 +99297,7 @@ var init_chunk_6H3V3WGJ = __esm({
99246
99297
 
99247
99298
  // ../studio-server/dist/chunk-ZPI6QXJH.js
99248
99299
  import { spawn as spawn8 } from "child_process";
99249
- import { createHash as createHash6, randomUUID as randomUUID6 } from "crypto";
99300
+ import { createHash as createHash6, randomUUID as randomUUID7 } from "crypto";
99250
99301
  import {
99251
99302
  existsSync as existsSync210,
99252
99303
  mkdirSync as mkdirSync15,
@@ -99570,7 +99621,7 @@ async function transcodeToCache(absoluteSourcePath, cachePath2, variant) {
99570
99621
  if (existsSync210(cachePath2)) return cachePath2;
99571
99622
  const cacheDir = dirname14(cachePath2);
99572
99623
  mkdirSync15(cacheDir, { recursive: true });
99573
- const tempPath = join29(cacheDir, `.tmp-${randomUUID6()}-${basename4(cachePath2)}`);
99624
+ const tempPath = join29(cacheDir, `.tmp-${randomUUID7()}-${basename4(cachePath2)}`);
99574
99625
  try {
99575
99626
  await runFfmpeg2(absoluteSourcePath, tempPath, variant);
99576
99627
  renameSync6(tempPath, cachePath2);
@@ -106487,7 +106538,7 @@ import { basename as basename5, join as join42 } from "path";
106487
106538
  import { mkdirSync as mkdirSync22, readdirSync as readdirSync32, readFileSync as readFileSync32, unlinkSync as unlinkSync5, writeFileSync as writeFileSync32 } from "fs";
106488
106539
  import { Buffer as Buffer2 } from "buffer";
106489
106540
  import { join as join52, relative as relative22 } from "path";
106490
- import { createHash as createHash22, randomUUID as randomUUID7 } from "crypto";
106541
+ import { createHash as createHash22, randomUUID as randomUUID8 } from "crypto";
106491
106542
  import { existsSync as existsSync37, readFileSync as readFileSync42, realpathSync as realpathSync6 } from "fs";
106492
106543
  import { randomUUID as randomUUID22 } from "crypto";
106493
106544
  import { dirname as dirname15, relative as relative32, resolve as resolve24, sep as sep7 } from "path";
@@ -106511,9 +106562,19 @@ import { join as join92 } from "path";
106511
106562
  import { streamSSE } from "hono/streaming";
106512
106563
  import { existsSync as existsSync72, readFileSync as readFileSync102, mkdirSync as mkdirSync42, unlinkSync as unlinkSync32, readdirSync as readdirSync52, statSync as statSync32 } from "fs";
106513
106564
  import { join as join102 } from "path";
106514
- import { existsSync as existsSync82, readFileSync as readFileSync112, writeFileSync as writeFileSync62, mkdirSync as mkdirSync52, statSync as statSync42 } from "fs";
106565
+ import {
106566
+ existsSync as existsSync82,
106567
+ mkdirSync as mkdirSync52,
106568
+ readFileSync as readFileSync112,
106569
+ readdirSync as readdirSync62,
106570
+ renameSync as renameSync22,
106571
+ rmSync as rmSync32,
106572
+ statSync as statSync42,
106573
+ unlinkSync as unlinkSync42,
106574
+ writeFileSync as writeFileSync62
106575
+ } from "fs";
106515
106576
  import { join as join112 } from "path";
106516
- import { createHash as createHash42 } from "crypto";
106577
+ import { createHash as createHash42, randomUUID as randomUUID32 } from "crypto";
106517
106578
  import { existsSync as existsSync92, readFileSync as readFileSync122, writeFileSync as writeFileSync72, mkdirSync as mkdirSync62 } from "fs";
106518
106579
  import { join as join122 } from "path";
106519
106580
  import { closeSync as closeSync32, constants as constants22, fstatSync as fstatSync2, openSync as openSync32, readSync } from "fs";
@@ -106940,7 +107001,7 @@ function fileContentVersion(content) {
106940
107001
  }
106941
107002
  function createWriteToken(requestToken) {
106942
107003
  const token = requestToken?.trim();
106943
- return token && token.length <= 200 ? token : randomUUID7();
107004
+ return token && token.length <= 200 ? token : randomUUID8();
106944
107005
  }
106945
107006
  function recordFileWriteReceipt(absPath, receipt) {
106946
107007
  const now = Date.now();
@@ -110026,6 +110087,46 @@ function registerRenderRoutes(api, adapter2) {
110026
110087
  return c3.json({ renders: files });
110027
110088
  });
110028
110089
  }
110090
+ function pruneThumbnailCache(cacheDir, protectedPaths, now = Date.now()) {
110091
+ if (!existsSync82(cacheDir)) return;
110092
+ const files = readdirSync62(cacheDir, { withFileTypes: true }).flatMap((entry) => {
110093
+ if (!entry.isFile()) return [];
110094
+ const path2 = join112(cacheDir, entry.name);
110095
+ try {
110096
+ const stats = statSync42(path2);
110097
+ return [{ path: path2, bytes: stats.size, mtimeMs: stats.mtimeMs }];
110098
+ } catch {
110099
+ return [];
110100
+ }
110101
+ });
110102
+ const retained = [];
110103
+ for (const file of files) {
110104
+ if (!protectedPaths.has(file.path) && now - file.mtimeMs > THUMBNAIL_CACHE_MAX_AGE_MS) {
110105
+ rmSync32(file.path, { force: true });
110106
+ } else {
110107
+ retained.push(file);
110108
+ }
110109
+ }
110110
+ let bytes = retained.reduce((total, file) => total + file.bytes, 0);
110111
+ for (const file of retained.sort((left, right) => left.mtimeMs - right.mtimeMs)) {
110112
+ if (bytes <= THUMBNAIL_CACHE_MAX_BYTES) break;
110113
+ if (protectedPaths.has(file.path)) continue;
110114
+ try {
110115
+ unlinkSync42(file.path);
110116
+ bytes -= file.bytes;
110117
+ } catch {
110118
+ }
110119
+ }
110120
+ }
110121
+ function writeThumbnailAtomically(path2, buffer) {
110122
+ const temporaryPath = `${path2}.${process.pid}.${randomUUID32()}.tmp`;
110123
+ try {
110124
+ writeFileSync62(temporaryPath, buffer, { flag: "wx" });
110125
+ renameSync22(temporaryPath, path2);
110126
+ } finally {
110127
+ rmSync32(temporaryPath, { force: true });
110128
+ }
110129
+ }
110029
110130
  function registerThumbnailRoutes(api, adapter2) {
110030
110131
  api.get("/projects/:id/thumbnail/*", async (c3) => {
110031
110132
  if (!adapter2.generateThumbnail) {
@@ -110046,6 +110147,8 @@ function registerThumbnailRoutes(api, adapter2) {
110046
110147
  const selector = url.searchParams.get("selector") || void 0;
110047
110148
  const format = url.searchParams.get("format") === "png" ? "png" : "jpeg";
110048
110149
  const contentType = format === "png" ? "image/png" : "image/jpeg";
110150
+ const requestedOutput = url.searchParams.get("output");
110151
+ const outputMode = requestedOutput === "source" || requestedOutput !== "preview" && format === "png" ? "source" : "preview";
110049
110152
  const rawSelectorIndex = Number.parseInt(url.searchParams.get("selectorIndex") || "0", 10);
110050
110153
  const selectorIndex = Number.isFinite(rawSelectorIndex) && rawSelectorIndex > 0 ? rawSelectorIndex : void 0;
110051
110154
  const urlVersion = url.searchParams.get("v") || "";
@@ -110083,37 +110186,62 @@ function registerThumbnailRoutes(api, adapter2) {
110083
110186
  const cacheDir = join112(project.dir, ".thumbnails");
110084
110187
  const selectorKey = selector ? `_${selector.replace(/[^a-zA-Z0-9_-]+/g, "_").slice(0, 80)}_${selectorIndex ?? 0}` : "";
110085
110188
  const urlVersionKey = urlVersion ? `_${urlVersion.replace(/[^a-zA-Z0-9_-]+/g, "_").slice(0, 32)}` : "";
110086
- const cacheKey = `${THUMBNAIL_CACHE_VERSION}${urlVersionKey}${manualEditsKey}${motionKey}${sourceKey}_${format}_${compPath.replace(/\//g, "_")}_${compW}x${compH}_${sourceMtime}_${seekTime.toFixed(2)}${selectorKey}.${format === "png" ? "png" : "jpg"}`;
110189
+ const outputScale = outputMode === "source" ? 1 : Math.min(1, THUMBNAIL_MAX_OUTPUT_WIDTH / compW, THUMBNAIL_MAX_OUTPUT_HEIGHT / compH);
110190
+ const outputWidth = Math.max(1, Math.round(compW * outputScale));
110191
+ const outputHeight = Math.max(1, Math.round(compH * outputScale));
110192
+ const cacheKey = `${THUMBNAIL_CACHE_VERSION}${urlVersionKey}${manualEditsKey}${motionKey}${sourceKey}_${format}_${outputMode}_${compPath.replace(/\//g, "_")}_${compW}x${compH}_${outputWidth}x${outputHeight}_${sourceMtime}_${seekTime.toFixed(2)}${selectorKey}.${format === "png" ? "png" : "jpg"}`;
110087
110193
  const cachePath2 = join112(cacheDir, cacheKey);
110194
+ if (!prunedCacheDirs.has(cacheDir)) {
110195
+ prunedCacheDirs.add(cacheDir);
110196
+ pruneThumbnailCache(
110197
+ cacheDir,
110198
+ /* @__PURE__ */ new Set([...thumbnailGenerationCoordinator.protectedKeys(), cachePath2])
110199
+ );
110200
+ }
110088
110201
  if (existsSync82(cachePath2)) {
110089
110202
  return new Response(new Uint8Array(readFileSync112(cachePath2)), {
110090
110203
  headers: { "Content-Type": contentType, "Cache-Control": "no-cache" }
110091
110204
  });
110092
110205
  }
110093
110206
  try {
110094
- const buffer = await adapter2.generateThumbnail({
110095
- project,
110096
- compPath,
110097
- seekTime,
110098
- width: compW,
110099
- height: compH,
110100
- previewUrl,
110101
- selector,
110102
- format,
110103
- selectorIndex
110104
- });
110207
+ const buffer = await thumbnailGenerationCoordinator.acquire(
110208
+ cachePath2,
110209
+ c3.req.raw.signal,
110210
+ async (signal) => {
110211
+ const generated = await adapter2.generateThumbnail({
110212
+ project,
110213
+ compPath,
110214
+ seekTime,
110215
+ width: compW,
110216
+ height: compH,
110217
+ outputWidth,
110218
+ outputHeight,
110219
+ previewUrl,
110220
+ selector,
110221
+ format,
110222
+ selectorIndex,
110223
+ signal
110224
+ });
110225
+ if (!generated) return null;
110226
+ if (!existsSync82(cacheDir)) mkdirSync52(cacheDir, { recursive: true });
110227
+ writeThumbnailAtomically(cachePath2, generated);
110228
+ return generated;
110229
+ }
110230
+ );
110105
110231
  if (!buffer) {
110106
110232
  return c3.json(
110107
110233
  { error: "Thumbnail generation failed \u2014 Chrome browser may not be available" },
110108
110234
  500
110109
110235
  );
110110
110236
  }
110111
- if (!existsSync82(cacheDir)) mkdirSync52(cacheDir, { recursive: true });
110112
- writeFileSync62(cachePath2, buffer);
110237
+ pruneThumbnailCache(cacheDir, thumbnailGenerationCoordinator.protectedKeys());
110113
110238
  return new Response(new Uint8Array(buffer), {
110114
110239
  headers: { "Content-Type": contentType, "Cache-Control": "no-cache" }
110115
110240
  });
110116
110241
  } catch (err) {
110242
+ if (err instanceof DOMException && err.name === "AbortError") {
110243
+ return new Response(null, { status: 499 });
110244
+ }
110117
110245
  const msg = err instanceof Error ? err.message : String(err);
110118
110246
  return c3.json({ error: `Thumbnail generation failed: ${msg}` }, 500);
110119
110247
  }
@@ -110592,6 +110720,18 @@ function createStudioApi(adapter2) {
110592
110720
  registerGlobalAssetRoutes(api);
110593
110721
  return api;
110594
110722
  }
110723
+ function thumbnailDeviceScaleFactor({
110724
+ width,
110725
+ height,
110726
+ outputWidth,
110727
+ outputHeight
110728
+ }) {
110729
+ const dimensions = [width, height, outputWidth, outputHeight];
110730
+ if (dimensions.some((value) => !Number.isFinite(value) || value <= 0)) {
110731
+ throw new RangeError("Thumbnail dimensions must be positive finite numbers");
110732
+ }
110733
+ return Math.min(1, outputWidth / width, outputHeight / height);
110734
+ }
110595
110735
  function createBackgroundRemovalJob(opts, render3) {
110596
110736
  const state = {
110597
110737
  id: opts.jobId,
@@ -110644,7 +110784,7 @@ function updateBackgroundRemovalProgress(state, event) {
110644
110784
  state.framesProcessed = event.index;
110645
110785
  state.avgMsPerFrame = event.avgMsPerFrame;
110646
110786
  }
110647
- var IGNORE_DIRS, SIGNATURE_TEXT_EXTENSIONS, SIGNATURE_EXCLUDED_DIRS, MAX_SIGNATURE_TEXT_BYTES, STUDIO_SIGNATURE_MANIFEST_PATHS, projectSignatureCache, COMPOSITION_ID_RE, MIME_TYPES2, SAMPLE_RATE, PEAK_COUNT, WAVEFORM_CACHE_VERSION, VIDEO_EXT2, AUDIO_EXT2, DEFAULT_KEEP_PER_FILE, RECEIPT_TTL_MS, receipts, CompositionInsertionError, differential, behavioral, GSAP_MUTATION_CAPABILITIES, GSAP_WRITER_MIGRATION, atomicCutTail, HOLD_SYNC_MUTATION_TYPES, REGEXP_SPECIALS, NON_RENDERED_TAGS, VARIABLES_PAYLOAD_ERROR, PROJECT_SIGNATURE_META, GSAP_CDN_VERSION, GSAP_CDN_SCRIPT, GSAP_CUSTOM_EASE_CDN_SCRIPT, GSAP_MOTION_PATH_CDN_SCRIPT, GSAP_CDN_FALLBACK_SCRIPT, VALID_RESOLUTIONS, THUMBNAIL_CACHE_VERSION, MAX_FONT_RESULTS, GOOGLE_FONTS_METADATA_URL, GOOGLE_FONTS_FETCH_TIMEOUT_MS, cachedFonts, cachedGoogleFonts, GOOGLE_FONT_FALLBACKS, VIDEO_EXTENSIONS2, IMAGE_EXTENSIONS, VIDEO_OUTPUT_EXTENSIONS, QUALITIES, DEVICES;
110787
+ var IGNORE_DIRS, SIGNATURE_TEXT_EXTENSIONS, SIGNATURE_EXCLUDED_DIRS, MAX_SIGNATURE_TEXT_BYTES, STUDIO_SIGNATURE_MANIFEST_PATHS, projectSignatureCache, COMPOSITION_ID_RE, MIME_TYPES2, SAMPLE_RATE, PEAK_COUNT, WAVEFORM_CACHE_VERSION, VIDEO_EXT2, AUDIO_EXT2, DEFAULT_KEEP_PER_FILE, RECEIPT_TTL_MS, receipts, CompositionInsertionError, differential, behavioral, GSAP_MUTATION_CAPABILITIES, GSAP_WRITER_MIGRATION, atomicCutTail, HOLD_SYNC_MUTATION_TYPES, REGEXP_SPECIALS, NON_RENDERED_TAGS, VARIABLES_PAYLOAD_ERROR, PROJECT_SIGNATURE_META, GSAP_CDN_VERSION, GSAP_CDN_SCRIPT, GSAP_CUSTOM_EASE_CDN_SCRIPT, GSAP_MOTION_PATH_CDN_SCRIPT, GSAP_CDN_FALLBACK_SCRIPT, VALID_RESOLUTIONS, ThumbnailGenerationCoordinator, thumbnailGenerationCoordinator, THUMBNAIL_CACHE_VERSION, THUMBNAIL_MAX_OUTPUT_WIDTH, THUMBNAIL_MAX_OUTPUT_HEIGHT, THUMBNAIL_CACHE_MAX_BYTES, THUMBNAIL_CACHE_MAX_AGE_MS, prunedCacheDirs, MAX_FONT_RESULTS, GOOGLE_FONTS_METADATA_URL, GOOGLE_FONTS_FETCH_TIMEOUT_MS, cachedFonts, cachedGoogleFonts, GOOGLE_FONT_FALLBACKS, VIDEO_EXTENSIONS2, IMAGE_EXTENSIONS, VIDEO_OUTPUT_EXTENSIONS, QUALITIES, DEVICES;
110648
110788
  var init_dist9 = __esm({
110649
110789
  "../studio-server/dist/index.js"() {
110650
110790
  "use strict";
@@ -110885,7 +111025,113 @@ var init_dist9 = __esm({
110885
111025
  })();
110886
111026
  </script>`;
110887
111027
  VALID_RESOLUTIONS = new Set(VALID_CANVAS_RESOLUTIONS);
111028
+ ThumbnailGenerationCoordinator = class {
111029
+ constructor(concurrency = 1) {
111030
+ this.concurrency = concurrency;
111031
+ if (!Number.isInteger(concurrency) || concurrency < 1) {
111032
+ throw new RangeError("Thumbnail concurrency must be a positive integer");
111033
+ }
111034
+ }
111035
+ concurrency;
111036
+ entries = /* @__PURE__ */ new Map();
111037
+ queue = [];
111038
+ activeEntries = /* @__PURE__ */ new Set();
111039
+ active = 0;
111040
+ acquire(key2, signal, work) {
111041
+ if (signal.aborted) return Promise.reject(new DOMException("Aborted", "AbortError"));
111042
+ let entry = this.entries.get(key2);
111043
+ if (!entry) {
111044
+ let resolve510;
111045
+ let reject;
111046
+ const promise = new Promise((resolvePromise, rejectPromise) => {
111047
+ resolve510 = resolvePromise;
111048
+ reject = rejectPromise;
111049
+ });
111050
+ entry = {
111051
+ key: key2,
111052
+ controller: new AbortController(),
111053
+ leases: 0,
111054
+ state: "queued",
111055
+ work,
111056
+ promise,
111057
+ resolve: resolve510,
111058
+ reject
111059
+ };
111060
+ this.entries.set(key2, entry);
111061
+ this.queue.push(entry);
111062
+ }
111063
+ entry.leases++;
111064
+ this.pump();
111065
+ return this.lease(entry, signal);
111066
+ }
111067
+ protectedKeys() {
111068
+ return /* @__PURE__ */ new Set([...this.entries.keys(), ...[...this.activeEntries].map((entry) => entry.key)]);
111069
+ }
111070
+ lease(entry, signal) {
111071
+ return new Promise((resolve510, reject) => {
111072
+ let released = false;
111073
+ const release4 = () => {
111074
+ if (released) return;
111075
+ released = true;
111076
+ signal.removeEventListener("abort", onAbort);
111077
+ entry.leases--;
111078
+ if (entry.leases > 0 || !this.entries.has(entry.key)) return;
111079
+ entry.controller.abort();
111080
+ if (this.entries.get(entry.key) === entry) this.entries.delete(entry.key);
111081
+ if (entry.state === "queued") {
111082
+ const index = this.queue.indexOf(entry);
111083
+ if (index >= 0) this.queue.splice(index, 1);
111084
+ entry.reject(new DOMException("Aborted", "AbortError"));
111085
+ }
111086
+ };
111087
+ const onAbort = () => {
111088
+ release4();
111089
+ reject(new DOMException("Aborted", "AbortError"));
111090
+ };
111091
+ signal.addEventListener("abort", onAbort, { once: true });
111092
+ entry.promise.then(
111093
+ (value) => {
111094
+ release4();
111095
+ resolve510(value);
111096
+ },
111097
+ (reason) => {
111098
+ release4();
111099
+ reject(reason);
111100
+ }
111101
+ );
111102
+ });
111103
+ }
111104
+ pump() {
111105
+ while (this.active < this.concurrency) {
111106
+ const entry = this.queue.shift();
111107
+ if (!entry) return;
111108
+ if (entry.leases === 0) continue;
111109
+ entry.state = "active";
111110
+ this.activeEntries.add(entry);
111111
+ this.active++;
111112
+ void this.run(entry);
111113
+ }
111114
+ }
111115
+ async run(entry) {
111116
+ try {
111117
+ entry.resolve(await entry.work(entry.controller.signal));
111118
+ } catch (error) {
111119
+ entry.reject(error);
111120
+ } finally {
111121
+ this.active--;
111122
+ this.activeEntries.delete(entry);
111123
+ if (this.entries.get(entry.key) === entry) this.entries.delete(entry.key);
111124
+ this.pump();
111125
+ }
111126
+ }
111127
+ };
111128
+ thumbnailGenerationCoordinator = new ThumbnailGenerationCoordinator(1);
110888
111129
  THUMBNAIL_CACHE_VERSION = "v4";
111130
+ THUMBNAIL_MAX_OUTPUT_WIDTH = 240;
111131
+ THUMBNAIL_MAX_OUTPUT_HEIGHT = 135;
111132
+ THUMBNAIL_CACHE_MAX_BYTES = 512 * 1024 * 1024;
111133
+ THUMBNAIL_CACHE_MAX_AGE_MS = 14 * 24 * 60 * 60 * 1e3;
111134
+ prunedCacheDirs = /* @__PURE__ */ new Set();
110889
111135
  MAX_FONT_RESULTS = 2e3;
110890
111136
  GOOGLE_FONTS_METADATA_URL = "https://fonts.google.com/metadata/fonts";
110891
111137
  GOOGLE_FONTS_FETCH_TIMEOUT_MS = 3e3;
@@ -124868,7 +125114,7 @@ import {
124868
125114
  import { tmpdir as tmpdir8 } from "os";
124869
125115
  import { join as join60, dirname as dirname25, resolve as resolve33 } from "path";
124870
125116
  import { totalmem as totalmem2 } from "os";
124871
- import { randomUUID as randomUUID8 } from "crypto";
125117
+ import { randomUUID as randomUUID9 } from "crypto";
124872
125118
  import { fileURLToPath as fileURLToPath5 } from "url";
124873
125119
  function sampleDirectoryBytes(dir) {
124874
125120
  let total = 0;
@@ -125227,7 +125473,7 @@ async function executeDiskCaptureWithAdaptiveRetry(options) {
125227
125473
  }
125228
125474
  function createRenderJob(config) {
125229
125475
  return {
125230
- id: randomUUID8(),
125476
+ id: randomUUID9(),
125231
125477
  config: {
125232
125478
  ...config,
125233
125479
  fps: toFps(config.fps),
@@ -132359,9 +132605,19 @@ function createStudioServer(options) {
132359
132605
  );
132360
132606
  }
132361
132607
  let page = null;
132608
+ const closePage = () => void page?.close().catch(() => {
132609
+ });
132610
+ opts.signal.addEventListener("abort", closePage, { once: true });
132362
132611
  try {
132363
132612
  page = await session.browser.newPage();
132364
- await page.setViewport({ width: opts.width || 1920, height: opts.height || 1080 });
132613
+ if (opts.signal.aborted) return null;
132614
+ const width = opts.width || 1920;
132615
+ const height = opts.height || 1080;
132616
+ await page.setViewport({
132617
+ width,
132618
+ height,
132619
+ deviceScaleFactor: thumbnailDeviceScaleFactor(opts)
132620
+ });
132365
132621
  await page.goto(opts.previewUrl, { waitUntil: "domcontentloaded", timeout: 1e4 });
132366
132622
  await page.waitForFunction(
132367
132623
  () => {
@@ -132404,12 +132660,15 @@ function createStudioServer(options) {
132404
132660
  );
132405
132661
  return screenshot;
132406
132662
  } catch (err) {
132407
- console.warn(
132408
- "[Studio] Thumbnail generation failed:",
132409
- err instanceof Error ? err.message : err
132410
- );
132663
+ if (!opts.signal.aborted) {
132664
+ console.warn(
132665
+ "[Studio] Thumbnail generation failed:",
132666
+ err instanceof Error ? err.message : err
132667
+ );
132668
+ }
132411
132669
  return null;
132412
132670
  } finally {
132671
+ opts.signal.removeEventListener("abort", closePage);
132413
132672
  await page?.close().catch(() => {
132414
132673
  });
132415
132674
  }
@@ -135057,6 +135316,7 @@ __export(studio_api_exports, {
135057
135316
  getElementScreenshotClip: () => getElementScreenshotClip,
135058
135317
  getMimeType: () => getMimeType,
135059
135318
  isSafePath: () => isSafePath,
135319
+ thumbnailDeviceScaleFactor: () => thumbnailDeviceScaleFactor,
135060
135320
  walkDir: () => walkDir
135061
135321
  });
135062
135322
  var init_studio_api = __esm({
@@ -136889,7 +137149,7 @@ var init_auth = __esm({
136889
137149
  });
136890
137150
 
136891
137151
  // src/utils/projectLink.ts
136892
- import { randomUUID as randomUUID9 } from "crypto";
137152
+ import { randomUUID as randomUUID10 } from "crypto";
136893
137153
  import { existsSync as existsSync75, mkdirSync as mkdirSync39, readFileSync as readFileSync46, writeFileSync as writeFileSync28 } from "fs";
136894
137154
  import { homedir as homedir16 } from "os";
136895
137155
  import { join as join83, resolve as resolve47 } from "path";
@@ -136940,7 +137200,7 @@ function ensureProjectId(absDir) {
136940
137200
  const path2 = resolve47(absDir);
136941
137201
  const existing = links[path2];
136942
137202
  if (existing) return existing.projectId;
136943
- const projectId = randomUUID9();
137203
+ const projectId = randomUUID10();
136944
137204
  links[path2] = { projectId, url: "" };
136945
137205
  writeProjectLinks(links);
136946
137206
  return projectId;
@@ -150620,7 +150880,7 @@ __export(feedback_exports, {
150620
150880
  default: () => feedback_default,
150621
150881
  examples: () => examples26
150622
150882
  });
150623
- import { randomUUID as randomUUID10 } from "crypto";
150883
+ import { randomUUID as randomUUID11 } from "crypto";
150624
150884
  import { resolve as resolve66 } from "path";
150625
150885
  import open from "open";
150626
150886
  function normalizeComment(raw) {
@@ -150773,7 +151033,7 @@ var init_feedback2 = __esm({
150773
151033
  }
150774
151034
  const comment = normalizeComment(args.comment);
150775
151035
  const doctorSummary = await getDoctorSummary();
150776
- const feedbackId = randomUUID10();
151036
+ const feedbackId = randomUUID11();
150777
151037
  const config = readConfig();
150778
151038
  const joinKeys = buildTelemetryJoinKeys({
150779
151039
  feedbackId,
@@ -159078,7 +159338,7 @@ var require_gaxios = __commonJS({
159078
159338
  var retry_js_1 = require_retry3();
159079
159339
  var stream_1 = __require("stream");
159080
159340
  var interceptor_js_1 = require_interceptor();
159081
- var randomUUID11 = async () => globalThis.crypto?.randomUUID() || (await import("crypto")).randomUUID();
159341
+ var randomUUID12 = async () => globalThis.crypto?.randomUUID() || (await import("crypto")).randomUUID();
159082
159342
  var HTTP_STATUS_NO_CONTENT = 204;
159083
159343
  var Gaxios = class {
159084
159344
  agentCache = /* @__PURE__ */ new Map();
@@ -159351,7 +159611,7 @@ var require_gaxios = __commonJS({
159351
159611
  */
159352
159612
  ["Blob", "File", "FormData"].includes(opts.data?.constructor?.name || "");
159353
159613
  if (opts.multipart?.length) {
159354
- const boundary = await randomUUID11();
159614
+ const boundary = await randomUUID12();
159355
159615
  preparedHeaders.set("content-type", `multipart/related; boundary=${boundary}`);
159356
159616
  opts.body = stream_1.Readable.from(this.getMultipartRequest(opts.multipart, boundary));
159357
159617
  } else if (shouldDirectlyPassData) {
@@ -193020,7 +193280,7 @@ __export(compare_exports, {
193020
193280
  parseCompareArgs: () => parseCompareArgs,
193021
193281
  prepareCompareVariantProjects: () => prepareCompareVariantProjects
193022
193282
  });
193023
- import { cpSync as cpSync6, existsSync as existsSync105, mkdirSync as mkdirSync51, mkdtempSync as mkdtempSync17, renameSync as renameSync15, rmSync as rmSync32, statSync as statSync36 } from "fs";
193283
+ import { cpSync as cpSync6, existsSync as existsSync105, mkdirSync as mkdirSync51, mkdtempSync as mkdtempSync17, renameSync as renameSync15, rmSync as rmSync33, statSync as statSync36 } from "fs";
193024
193284
  import { tmpdir as tmpdir17 } from "os";
193025
193285
  import { basename as basename28, dirname as dirname55, extname as extname23, join as join110 } from "path";
193026
193286
  function defaultLabelForPath(input2) {
@@ -193140,7 +193400,7 @@ function stageHtmlVariant(variant) {
193140
193400
  stagedDir
193141
193401
  };
193142
193402
  } catch (err) {
193143
- rmSync32(stagedDir, { recursive: true, force: true });
193403
+ rmSync33(stagedDir, { recursive: true, force: true });
193144
193404
  throw err;
193145
193405
  }
193146
193406
  }
@@ -193169,7 +193429,7 @@ function prepareCompareVariantProjects(variants) {
193169
193429
  } catch (err) {
193170
193430
  for (const variant of prepared) {
193171
193431
  if (variant.stagedDir) {
193172
- rmSync32(variant.stagedDir, { recursive: true, force: true });
193432
+ rmSync33(variant.stagedDir, { recursive: true, force: true });
193173
193433
  }
193174
193434
  }
193175
193435
  throw err;
@@ -193178,7 +193438,7 @@ function prepareCompareVariantProjects(variants) {
193178
193438
  function cleanupPreparedCompareVariants(variants) {
193179
193439
  for (const variant of variants) {
193180
193440
  if (variant.stagedDir) {
193181
- rmSync32(variant.stagedDir, { recursive: true, force: true });
193441
+ rmSync33(variant.stagedDir, { recursive: true, force: true });
193182
193442
  }
193183
193443
  }
193184
193444
  }
@@ -193250,7 +193510,7 @@ async function renderCompareSheet(parsed) {
193250
193510
  return buildCompareSuccessPayload(parsed.outPath, variants, capResult);
193251
193511
  } finally {
193252
193512
  cleanupPreparedCompareVariants(prepared);
193253
- rmSync32(frameDir, { recursive: true, force: true });
193513
+ rmSync33(frameDir, { recursive: true, force: true });
193254
193514
  }
193255
193515
  }
193256
193516
  function printJson2(payload) {
@@ -197966,7 +198226,7 @@ __export(state_exports, {
197966
198226
  stateFilePath: () => stateFilePath,
197967
198227
  writeStackOutputs: () => writeStackOutputs
197968
198228
  });
197969
- import { existsSync as existsSync113, mkdirSync as mkdirSync58, readdirSync as readdirSync38, readFileSync as readFileSync77, rmSync as rmSync33, writeFileSync as writeFileSync53 } from "fs";
198229
+ import { existsSync as existsSync113, mkdirSync as mkdirSync58, readdirSync as readdirSync38, readFileSync as readFileSync77, rmSync as rmSync34, writeFileSync as writeFileSync53 } from "fs";
197970
198230
  import { dirname as dirname56, join as join121 } from "path";
197971
198231
  function stateFilePath(stackName = DEFAULT_STACK_NAME, cwd = process.cwd()) {
197972
198232
  return join121(cwd, STATE_DIR_NAME, `${STATE_FILE_PREFIX}${stackName}.json`);
@@ -197988,7 +198248,7 @@ function readStackOutputs(stackName = DEFAULT_STACK_NAME, cwd = process.cwd()) {
197988
198248
  }
197989
198249
  function deleteStackOutputs(stackName = DEFAULT_STACK_NAME, cwd = process.cwd()) {
197990
198250
  const path2 = stateFilePath(stackName, cwd);
197991
- if (existsSync113(path2)) rmSync33(path2);
198251
+ if (existsSync113(path2)) rmSync34(path2);
197992
198252
  }
197993
198253
  function listStackNames(cwd = process.cwd()) {
197994
198254
  const dir = join121(cwd, STATE_DIR_NAME);
@@ -203093,7 +203353,7 @@ var init_parseFigmaRef = __esm({
203093
203353
  });
203094
203354
 
203095
203355
  // ../core/dist/figma/freeze.js
203096
- import { copyFileSync as copyFileSync11, mkdirSync as mkdirSync61, rmSync as rmSync34, statSync as statSync39, writeFileSync as writeFileSync55 } from "fs";
203356
+ import { copyFileSync as copyFileSync11, mkdirSync as mkdirSync61, rmSync as rmSync35, statSync as statSync39, writeFileSync as writeFileSync55 } from "fs";
203097
203357
  import { dirname as dirname59 } from "path";
203098
203358
  function exceedsFreezeCap(byteLength) {
203099
203359
  return byteLength > MAX_FREEZE_BYTES;
@@ -203109,7 +203369,7 @@ function freezeBytes(bytes, destPath) {
203109
203369
  } catch (err) {
203110
203370
  if (err.code !== "EEXIST")
203111
203371
  throw err;
203112
- rmSync34(destPath);
203372
+ rmSync35(destPath);
203113
203373
  writeFileSync55(destPath, bytes, { flag: "wx" });
203114
203374
  }
203115
203375
  return bytes.length;