@hyperframes/studio-server 0.8.191 → 0.8.193

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.
@@ -7,7 +7,7 @@ import {
7
7
 
8
8
  // src/history/projectHistory.ts
9
9
  import { randomUUID as randomUUID7 } from "crypto";
10
- import { mkdir as mkdir2, readdir as readdir3, rm as rm3, rmdir, writeFile } from "fs/promises";
10
+ import { mkdir as mkdir2, readdir as readdir3, rm as rm3, rmdir, stat as stat2, writeFile } from "fs/promises";
11
11
  import {
12
12
  existsSync as existsSync3,
13
13
  lstatSync as lstatSync4,
@@ -37,9 +37,9 @@ function hashVersion(hex) {
37
37
  function hashOfVersion(version) {
38
38
  return /^"sha256:([0-9a-f]{64})"$/.exec(version)?.[1];
39
39
  }
40
- function settledFileTag(stat2, now = Date.now()) {
41
- if (now - stat2.ctimeMs < 3e3) return null;
42
- return [stat2.ino, stat2.ctimeMs, stat2.size].map((n) => n.toString(36)).join("-");
40
+ function settledFileTag(stat4, now = Date.now()) {
41
+ if (now - stat4.ctimeMs < 3e3) return null;
42
+ return [stat4.ino, stat4.ctimeMs, stat4.size].map((n) => n.toString(36)).join("-");
43
43
  }
44
44
  function createWriteToken(requestToken) {
45
45
  const token = requestToken?.trim();
@@ -345,8 +345,8 @@ function signatureEntryStat(projectDir, entry, file, outputs) {
345
345
  if (isSkippedEntry(entry) || !isPathWithin(projectDir, file)) return null;
346
346
  if (isRenderOutput(projectDir, file, outputs)) return null;
347
347
  try {
348
- const stat2 = lstatSync(file);
349
- return stat2.isSymbolicLink() ? null : stat2;
348
+ const stat4 = lstatSync(file);
349
+ return stat4.isSymbolicLink() ? null : stat4;
350
350
  } catch {
351
351
  return null;
352
352
  }
@@ -360,17 +360,17 @@ function collectProjectSignatureFiles(projectDir, dir, files, outputs = readRend
360
360
  }
361
361
  for (const entry of entries) {
362
362
  const file = resolve2(dir, entry);
363
- const stat2 = signatureEntryStat(projectDir, entry, file, outputs);
364
- if (!stat2) continue;
365
- if (stat2.isDirectory()) {
363
+ const stat4 = signatureEntryStat(projectDir, entry, file, outputs);
364
+ if (!stat4) continue;
365
+ if (stat4.isDirectory()) {
366
366
  collectProjectSignatureFiles(projectDir, file, files, outputs);
367
- } else if (stat2.isFile()) {
367
+ } else if (stat4.isFile()) {
368
368
  files.push({
369
369
  file,
370
- mtimeMs: stat2.mtimeMs,
371
- ctimeMs: stat2.ctimeMs,
372
- size: stat2.size,
373
- textContentEligible: isTextContentEligible(file, stat2.size)
370
+ mtimeMs: stat4.mtimeMs,
371
+ ctimeMs: stat4.ctimeMs,
372
+ size: stat4.size,
373
+ textContentEligible: isTextContentEligible(file, stat4.size)
374
374
  });
375
375
  }
376
376
  }
@@ -380,19 +380,19 @@ function collectProjectSignatureManifestFiles(projectDir, files) {
380
380
  for (const manifestPath of STUDIO_SIGNATURE_MANIFEST_PATHS) {
381
381
  const file = resolve2(projectDir, manifestPath);
382
382
  if (seen.has(file) || !isPathWithin(projectDir, file)) continue;
383
- let stat2;
383
+ let stat4;
384
384
  try {
385
- stat2 = lstatSync(file);
385
+ stat4 = lstatSync(file);
386
386
  } catch {
387
387
  continue;
388
388
  }
389
- if (stat2.isSymbolicLink() || !stat2.isFile()) continue;
389
+ if (stat4.isSymbolicLink() || !stat4.isFile()) continue;
390
390
  files.push({
391
391
  file,
392
- mtimeMs: stat2.mtimeMs,
393
- ctimeMs: stat2.ctimeMs,
394
- size: stat2.size,
395
- textContentEligible: isTextContentEligible(file, stat2.size)
392
+ mtimeMs: stat4.mtimeMs,
393
+ ctimeMs: stat4.ctimeMs,
394
+ size: stat4.size,
395
+ textContentEligible: isTextContentEligible(file, stat4.size)
396
396
  });
397
397
  seen.add(file);
398
398
  }
@@ -540,11 +540,21 @@ async function openBlobStore(dir) {
540
540
  total += size;
541
541
  return hash;
542
542
  },
543
+ reference: (absPath) => hashFile(absPath),
543
544
  has: (hash) => sizes.has(hash),
544
545
  read: async (hash) => readBlob(dir, hash),
545
546
  writeTo: async (hash, absPath, beforeReplace) => cloneOrCopy(pathOf(hash), absPath, beforeReplace),
546
547
  bytes: () => total,
547
548
  size: (hash) => sizes.get(hash) ?? 0,
549
+ async drop(hashes) {
550
+ for (const hash of hashes) {
551
+ const size = sizes.get(hash);
552
+ if (size === void 0) continue;
553
+ await rm(pathOf(hash), { force: true });
554
+ sizes.delete(hash);
555
+ total -= size;
556
+ }
557
+ },
548
558
  async prune(keep) {
549
559
  for (const [hash, size] of sizes) {
550
560
  if (keep.has(hash)) continue;
@@ -556,6 +566,42 @@ async function openBlobStore(dir) {
556
566
  };
557
567
  }
558
568
 
569
+ // src/history/uncopied.ts
570
+ import { extname as extname2 } from "path";
571
+ var MEDIA_EXTENSIONS = /* @__PURE__ */ new Set([
572
+ ".mp4",
573
+ ".mov",
574
+ ".m4v",
575
+ ".webm",
576
+ ".mkv",
577
+ ".avi",
578
+ ".mxf",
579
+ ".m2ts",
580
+ ".mp3",
581
+ ".wav",
582
+ ".aac",
583
+ ".m4a",
584
+ ".flac",
585
+ ".ogg",
586
+ ".opus"
587
+ ]);
588
+ var COPY_NOTHING_ABOVE_BYTES = 64 * 1024 ** 2;
589
+ var isUncopied = (path, size) => size > COPY_NOTHING_ABOVE_BYTES || MEDIA_EXTENSIONS.has(extname2(path).toLowerCase());
590
+ function uncopiedOnlyHashes(log, sizeOf) {
591
+ const copied = /* @__PURE__ */ new Set();
592
+ const uncopied = /* @__PURE__ */ new Set();
593
+ const note = (path, hash) => {
594
+ if (hash) (isUncopied(path, sizeOf(hash)) ? uncopied : copied).add(hash);
595
+ };
596
+ for (const [path, hash] of log.baseline) note(path, hash);
597
+ for (const entry of log.entries)
598
+ for (const file of entry.files) {
599
+ note(file.path, file.before);
600
+ note(file.path, file.after);
601
+ }
602
+ return [...uncopied].filter((hash) => !copied.has(hash));
603
+ }
604
+
559
605
  // src/history/pruneHistories.ts
560
606
  import { randomUUID as randomUUID5 } from "crypto";
561
607
  import { readdirSync as readdirSync2, readFileSync as readFileSync5, renameSync as renameSync3, statSync as statSync3, writeFileSync as writeFileSync3 } from "fs";
@@ -740,13 +786,13 @@ function projectGone(dir, id, dev, tempDir) {
740
786
  }
741
787
  function diskStillHere(dir, dev, tempDir) {
742
788
  let at = dir;
743
- let stat2 = statSync3(at, { throwIfNoEntry: false });
744
- while (!stat2) {
789
+ let stat4 = statSync3(at, { throwIfNoEntry: false });
790
+ while (!stat4) {
745
791
  if (dirname2(at) === at) return false;
746
792
  at = dirname2(at);
747
- stat2 = statSync3(at, { throwIfNoEntry: false });
793
+ stat4 = statSync3(at, { throwIfNoEntry: false });
748
794
  }
749
- if (typeof dev === "number") return stat2.dev === dev;
795
+ if (typeof dev === "number") return stat4.dev === dev;
750
796
  return at === dir || isSafePath(tempDir, at);
751
797
  }
752
798
  function lastUsed(home) {
@@ -866,12 +912,12 @@ function stillPendingAfterUnlink(item) {
866
912
  let descriptor;
867
913
  try {
868
914
  descriptor = openSync(item.path, constants2.O_RDONLY | constants2.O_NOFOLLOW);
869
- const stat2 = fstatSync(descriptor);
870
- if (!stat2.isFile() || stat2.isSymbolicLink()) return false;
915
+ const stat4 = fstatSync(descriptor);
916
+ if (!stat4.isFile() || stat4.isSymbolicLink()) return false;
871
917
  const hash = createHash4("sha256").update(readFileSync6(descriptor)).digest("hex");
872
918
  if (hash !== item.hash) return false;
873
919
  const current = lstatSync2(item.path);
874
- if (current.dev !== stat2.dev || current.ino !== stat2.ino) return false;
920
+ if (current.dev !== stat4.dev || current.ino !== stat4.ino) return false;
875
921
  unlinkSync(item.path);
876
922
  return false;
877
923
  } catch (error) {
@@ -988,8 +1034,8 @@ function ownerPid(owner) {
988
1034
  const pid = owner.pid;
989
1035
  return typeof pid === "number" && Number.isSafeInteger(pid) && pid > 0 ? pid : null;
990
1036
  }
991
- function ordinaryOwner(stat2) {
992
- return stat2.isFile() && stat2.nlink === 1;
1037
+ function ordinaryOwner(stat4) {
1038
+ return stat4.isFile() && stat4.nlink === 1;
993
1039
  }
994
1040
  function sameOwnerFile(before, current) {
995
1041
  return current.dev === before.dev && current.ino === before.ino;
@@ -1063,8 +1109,8 @@ function runsOnDisk(root) {
1063
1109
  return entries.filter((entry) => entry.isDirectory() && UUID.test(entry.name)).flatMap((entry) => {
1064
1110
  const path = join6(root, entry.name);
1065
1111
  try {
1066
- const stat2 = lstatSync3(path);
1067
- return [{ path, stat: stat2 }];
1112
+ const stat4 = lstatSync3(path);
1113
+ return [{ path, stat: stat4 }];
1068
1114
  } catch {
1069
1115
  return [];
1070
1116
  }
@@ -1078,10 +1124,10 @@ function planSnapshotCleanup(project, state, now = Date.now()) {
1078
1124
  const runs = runsOnDisk(root);
1079
1125
  for (const run of runs.slice(0, SNAPSHOT_RUNS_TO_KEEP)) keep.add(run.path);
1080
1126
  const pending = [];
1081
- for (const { path, stat: stat2 } of runs) {
1082
- if (keep.has(path) || now - stat2.mtimeMs < SNAPSHOT_GRACE_MS || liveOwner(path)) continue;
1127
+ for (const { path, stat: stat4 } of runs) {
1128
+ if (keep.has(path) || now - stat4.mtimeMs < SNAPSHOT_GRACE_MS || liveOwner(path)) continue;
1083
1129
  try {
1084
- const run = { path, dev: stat2.dev, ino: stat2.ino, mtimeMs: stat2.mtimeMs };
1130
+ const run = { path, dev: stat4.dev, ino: stat4.ino, mtimeMs: stat4.mtimeMs };
1085
1131
  if (!unchangedRun(project, run)) continue;
1086
1132
  pending.push(run);
1087
1133
  retireReferences(records2, root, path);
@@ -1726,6 +1772,7 @@ var Engine = class {
1726
1772
  );
1727
1773
  if (!log) return this.firstOpen();
1728
1774
  this.log = withoutHiddenPaths(log);
1775
+ await this.reclaimUncopied();
1729
1776
  if (!this.log.keepsLedger) await this.takeInUnnamedLedger();
1730
1777
  const cache = this.readStatCache();
1731
1778
  const last = this.log.entries.at(-1)?.id ?? START;
@@ -1739,6 +1786,10 @@ var Engine = class {
1739
1786
  this.reopenClosedWindow();
1740
1787
  await this.settleAll();
1741
1788
  }
1789
+ /** Copies an older version took of files that are no longer copied; text and small files are never touched. */
1790
+ async reclaimUncopied() {
1791
+ await this.blobs.drop(uncopiedOnlyHashes(this.log, (hash) => this.blobs.size(hash)));
1792
+ }
1742
1793
  hearWrites() {
1743
1794
  const realDir = realFilePath(this.dir);
1744
1795
  this.stopHearing = onFileOverwritten((absPath, version, bytes) => {
@@ -1753,7 +1804,8 @@ var Engine = class {
1753
1804
  chainBackFrom(this.overwritten.get(path), replaced).set(after, replaced)
1754
1805
  );
1755
1806
  this.queue(async () => {
1756
- if (!this.blobs.has(replaced)) await this.storeBytes(bytes);
1807
+ if (!this.blobs.has(replaced) && !isUncopied(path, bytes.length))
1808
+ await this.storeBytes(bytes);
1757
1809
  }).catch((error) => this.options.onError?.(error));
1758
1810
  });
1759
1811
  }
@@ -1832,7 +1884,8 @@ var Engine = class {
1832
1884
  }
1833
1885
  async recordAdopted(file, sweptAt, stored) {
1834
1886
  if (stored === null || this.whereFolder() !== "here") return;
1835
- const hash = this.blobs.has(stored) ? stored : await this.storeIfPresent(file.path);
1887
+ const kept = this.blobs.has(stored) || isUncopied(file.path, file.size);
1888
+ const hash = kept ? stored : await this.storeIfPresent(file.path);
1836
1889
  if (hash === null) return;
1837
1890
  const change = { path: file.path, before: null, after: hash };
1838
1891
  const found = this.overwrittenBy(change, void 0, /* @__PURE__ */ new Set());
@@ -1908,22 +1961,24 @@ var Engine = class {
1908
1961
  /** The file's hash once copied in; null when it was removed before the copy (the next sweep records that). */
1909
1962
  async storeIfPresent(path) {
1910
1963
  try {
1911
- return await this.blobs.put(join8(this.dir, path));
1964
+ const abs = join8(this.dir, path);
1965
+ const { size } = await stat2(abs);
1966
+ return await (isUncopied(path, size) ? this.blobs.reference(abs) : this.blobs.put(abs));
1912
1967
  } catch (error) {
1913
1968
  if (error.code === "ENOENT") return null;
1914
1969
  throw error;
1915
1970
  }
1916
1971
  }
1917
- async observe(path, stat2, at) {
1972
+ async observe(path, stat4, at) {
1918
1973
  const known = this.tracked.get(path) ?? { hash: null, stat: null };
1919
- if (stat2 && known.stat === stat2) return false;
1974
+ if (stat4 && known.stat === stat4) return false;
1920
1975
  const heard = this.overwritten.get(path);
1921
1976
  const hash = await this.storeIfPresent(path);
1922
1977
  if (hash === null || this.whereFolder() !== "here") return false;
1923
- this.tracked.set(path, { hash, stat: stat2 });
1978
+ this.tracked.set(path, { hash, stat: stat4 });
1924
1979
  this.forgetWritesBefore(path, hash, heard);
1925
1980
  if (known.hash !== hash) await this.record(path, known.hash, hash, at);
1926
- return known.hash !== hash || known.stat !== stat2;
1981
+ return known.hash !== hash || known.stat !== stat4;
1927
1982
  }
1928
1983
  async record(path, before, after, at) {
1929
1984
  const endedBeforeThisWrite = this.windows.filter((open) => !takesWrite(open, at));
@@ -2221,9 +2276,9 @@ var Engine = class {
2221
2276
  const parts = path.split("/");
2222
2277
  for (let depth = 1; depth <= parts.length; depth += 1) {
2223
2278
  const at = parts.slice(0, depth).join("/");
2224
- const stat2 = lstatSync4(join8(this.dir, at), { throwIfNoEntry: false });
2225
- if (!stat2) return void 0;
2226
- if (at === path || !stat2.isDirectory()) return { at, stat: stat2 };
2279
+ const stat4 = lstatSync4(join8(this.dir, at), { throwIfNoEntry: false });
2280
+ if (!stat4) return void 0;
2281
+ if (at === path || !stat4.isDirectory()) return { at, stat: stat4 };
2227
2282
  }
2228
2283
  return void 0;
2229
2284
  }
@@ -2242,6 +2297,11 @@ var Engine = class {
2242
2297
  async writeAs(who, label, target, extra) {
2243
2298
  this.assertWritable();
2244
2299
  const changes = [...target].filter(([path, hash]) => (this.tracked.get(path)?.hash ?? null) !== hash).sort(([, a], [, b]) => Number(a !== null) - Number(b !== null));
2300
+ const gone = changes.find(([, hash]) => hash !== null && !this.blobs.has(hash));
2301
+ if (gone)
2302
+ throw new Error(
2303
+ `An earlier version of ${gone[0]} is not kept, so this cannot be brought back.`
2304
+ );
2245
2305
  const deleted = new Set(changes.filter(([, hash]) => hash === null).map(([path]) => path));
2246
2306
  const blocked = changes.filter(([, hash]) => hash !== null).map(([path]) => this.inTheWay(path, deleted)).find(Boolean);
2247
2307
  if (blocked) throw new Error(`${blocked} is in the way; move or delete it, then try again.`);
@@ -2349,6 +2409,18 @@ var Engine = class {
2349
2409
  [...this.tracked.keys()].map((path) => [path, null])
2350
2410
  );
2351
2411
  for (const [path, hash] of files) target.set(path, hash);
2412
+ if (point === START && this.adopting.size > 0) {
2413
+ throw new HistoryRefusedError(
2414
+ "The project's starting copy is still being saved; try again in a moment. Nothing was changed."
2415
+ );
2416
+ }
2417
+ const isProjectFile = (path) => !KEPT_HIDDEN_PATHS.has(path);
2418
+ const keepsAny = [...target].some(([path, hash]) => hash !== null && isProjectFile(path));
2419
+ if ([...this.tracked.keys()].some(isProjectFile) && !keepsAny) {
2420
+ throw new HistoryRefusedError(
2421
+ "Restoring this point would remove every file in this project, so nothing was changed."
2422
+ );
2423
+ }
2352
2424
  return this.writeAs(who, label, target, { restoredTo: point });
2353
2425
  }
2354
2426
  /** Settles, then runs `task` with its writes labelled `writeToken`. */
@@ -2408,6 +2480,9 @@ var Engine = class {
2408
2480
  if ((await readdir3(dest).catch(missingIsEmpty)).length > 0)
2409
2481
  throw new Error(`Checkout writes into an empty folder only: ${dest}`);
2410
2482
  const files = manifestAround(this.log, entryId, side);
2483
+ for (const [path, hash] of files)
2484
+ if (!this.blobs.has(hash))
2485
+ throw new Error(`The version of ${path} from then is not kept.`);
2411
2486
  try {
2412
2487
  for (const [path, hash] of files) await this.blobs.writeTo(hash, join8(dest, path));
2413
2488
  } catch (error) {
@@ -2465,6 +2540,12 @@ var HistoryClosedError = class extends Error {
2465
2540
  this.name = "HistoryClosedError";
2466
2541
  }
2467
2542
  };
2543
+ var HistoryRefusedError = class extends Error {
2544
+ constructor(message) {
2545
+ super(message);
2546
+ this.name = "HistoryRefusedError";
2547
+ }
2548
+ };
2468
2549
  async function openProjectHistory(options) {
2469
2550
  if (!existsSync3(options.projectDir))
2470
2551
  throw new HistoryClosedError(`${options.projectDir} is gone or now another project.`);
@@ -2513,6 +2594,66 @@ function readProjectHistory(options) {
2513
2594
  };
2514
2595
  }
2515
2596
 
2597
+ // src/history/reclaimUncopied.ts
2598
+ import { readdirSync as readdirSync5 } from "fs";
2599
+ import { rm as rm4, stat as stat3 } from "fs/promises";
2600
+ import { join as join9 } from "path";
2601
+ function historyIds(historyRoot) {
2602
+ try {
2603
+ return readdirSync5(historyRoot).filter(isHistoryId);
2604
+ } catch {
2605
+ return [];
2606
+ }
2607
+ }
2608
+ function namedHashes(log) {
2609
+ const hashes = new Set(log.baseline.values());
2610
+ for (const entry of log.entries) {
2611
+ for (const file of entry.files) {
2612
+ if (file.before) hashes.add(file.before);
2613
+ if (file.after) hashes.add(file.after);
2614
+ }
2615
+ }
2616
+ return hashes;
2617
+ }
2618
+ async function blobSizes(blobs, hashes) {
2619
+ const sizes = /* @__PURE__ */ new Map();
2620
+ for (const hash of hashes)
2621
+ sizes.set(hash, (await stat3(blobPath(blobs, hash)).catch(() => null))?.size ?? 0);
2622
+ return sizes;
2623
+ }
2624
+ async function reclaimOne(home, dryRun) {
2625
+ const log = readLog(join9(home, "log.jsonl"), () => {
2626
+ });
2627
+ if (!log) return 0;
2628
+ const blobs = join9(home, "blobs");
2629
+ const sizes = await blobSizes(blobs, namedHashes(log));
2630
+ let bytes = 0;
2631
+ for (const hash of uncopiedOnlyHashes(log, (known) => sizes.get(known) ?? 0)) {
2632
+ const size = sizes.get(hash) ?? 0;
2633
+ if (size === 0) continue;
2634
+ bytes += size;
2635
+ if (!dryRun) await rm4(blobPath(blobs, hash), { force: true });
2636
+ }
2637
+ return bytes;
2638
+ }
2639
+ async function reclaimUncopiedBlobs(historyRoot, { dryRun = false, onError } = {}) {
2640
+ const reclaimed = [];
2641
+ for (const id of historyIds(historyRoot)) {
2642
+ const home = join9(historyRoot, id);
2643
+ let release;
2644
+ try {
2645
+ release = await takeHistoryOwnership(home, 0);
2646
+ const bytes = await reclaimOne(home, dryRun);
2647
+ if (bytes > 0) reclaimed.push({ id, bytes });
2648
+ } catch (error) {
2649
+ if (!(error instanceof HistoryBusyError)) onError?.(error);
2650
+ } finally {
2651
+ release?.();
2652
+ }
2653
+ }
2654
+ return reclaimed;
2655
+ }
2656
+
2516
2657
  // src/history/historyCache.ts
2517
2658
  function historyCache(open) {
2518
2659
  const histories = /* @__PURE__ */ new Map();
@@ -2569,7 +2710,9 @@ export {
2569
2710
  MAX_WINDOW_IDLE_MS,
2570
2711
  UNDO_MODES,
2571
2712
  HistoryClosedError,
2713
+ HistoryRefusedError,
2572
2714
  openProjectHistory,
2573
2715
  readProjectHistory,
2716
+ reclaimUncopiedBlobs,
2574
2717
  historyCache
2575
2718
  };
@@ -8,6 +8,7 @@ import { isAbsolute, relative, resolve } from "path";
8
8
  import { promisify } from "util";
9
9
  import { isAudibleVideoElement } from "@hyperframes/core/audible-video";
10
10
  import { formatAudioGain, MAX_AUDIO_GAIN_DB } from "@hyperframes/core/audio-gain";
11
+ import { decodeAuthoredAttribute } from "@hyperframes/parsers";
11
12
  import { findFfBinary } from "@hyperframes/parsers/ff-binaries";
12
13
  import { readMediaOffsetSeconds } from "@hyperframes/parsers/media-duration";
13
14
 
@@ -46,7 +47,14 @@ function attributeFromMatch(match, absoluteOffset) {
46
47
  const whole = match[0];
47
48
  const localValueStart = attributeValueStart(whole);
48
49
  const valueStart = absoluteOffset + match.index + localValueStart;
49
- return [name, { value, valueStart, valueEnd: valueStart + value.length }];
50
+ return [
51
+ name,
52
+ {
53
+ value: decodeAuthoredAttribute(value),
54
+ valueStart,
55
+ valueEnd: valueStart + value.length
56
+ }
57
+ ];
50
58
  }
51
59
  function matchedAttributeValue(match) {
52
60
  return match[2] ?? match[3] ?? match[4];
@@ -11,7 +11,7 @@ import {
11
11
  requiredFfmpeg,
12
12
  resolveLocalAudioPath,
13
13
  updateAudioVolume
14
- } from "../chunk-ERQWOZ7E.js";
14
+ } from "../chunk-QZWVJKLI.js";
15
15
  import "../chunk-QF7MFJJU.js";
16
16
  import "../chunk-SVQFE44C.js";
17
17
  import "../chunk-L3HJVTQE.js";
@@ -1,7 +1,7 @@
1
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-CAiGJucG.js';
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-C0DZX2Mo.js';
3
3
  import './mediaCodecMap.js';
4
4
  import 'sharp';
5
5
  import '@hyperframes/parsers';
6
6
  import '@hyperframes/core/compiler';
7
- import '../projectHistory-ByC9sa3X.js';
7
+ import '../projectHistory-C18HDvac.js';
@@ -1,5 +1,15 @@
1
- import { P as ProjectHistory, f as HistoryTarget, g as HistoryWho } from '../projectHistory-ByC9sa3X.js';
2
- 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, h as HistoryWindow, M as MAX_WINDOW_IDLE_MS, i as ProjectHistoryOptions, j as ProjectHistoryView, U as UNDO_MODES, k as UndoMode, o as openProjectHistory, r as readProjectHistory } from '../projectHistory-ByC9sa3X.js';
1
+ import { P as ProjectHistory, g as HistoryTarget, h as HistoryWho } from '../projectHistory-C18HDvac.js';
2
+ 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 HistoryRefusedError, f as HistoryResult, i as HistoryWindow, M as MAX_WINDOW_IDLE_MS, j as ProjectHistoryOptions, k as ProjectHistoryView, U as UNDO_MODES, l as UndoMode, o as openProjectHistory, r as readProjectHistory } from '../projectHistory-C18HDvac.js';
3
+
4
+ interface ReclaimedHistory {
5
+ id: string;
6
+ bytes: number;
7
+ }
8
+ /** Drops the media copies older versions left in every history no running process holds; the same rule as on open. */
9
+ declare function reclaimUncopiedBlobs(historyRoot: string, { dryRun, onError }?: {
10
+ dryRun?: boolean;
11
+ onError?: (error: unknown) => void;
12
+ }): Promise<ReclaimedHistory[]>;
3
13
 
4
14
  declare class HistoryBusyError extends Error {
5
15
  readonly pid: number;
@@ -138,4 +148,4 @@ declare function registerRenderOutput(projectDir: string, output: string): Promi
138
148
  /** Explicit outputs remain user evidence even when reusing a managed UUID folder. */
139
149
  declare function retainSnapshotOutput(project: string, path: string): void;
140
150
 
141
- export { type ArtifactCapture, type ArtifactInput, type ArtifactRecord, HistoryBusyError, HistoryIdError, HistoryTarget, HistoryWho, ProjectHistory, type ProjectHistoryRecord, type PrunedHistory, type RenderRecord, type VideoState, artifactDirectory, beginArtifactCapture, finishSnapshotRun, forgetRecordedArtifacts, historyCache, listProjectHistories, pruneGoneProjectHistories, recordArtifact, recordArtifacts, recordRenderSuccess, registerRenderOutput, retainSnapshotOutput };
151
+ export { type ArtifactCapture, type ArtifactInput, type ArtifactRecord, HistoryBusyError, HistoryIdError, HistoryTarget, HistoryWho, ProjectHistory, type ProjectHistoryRecord, type PrunedHistory, type ReclaimedHistory, type RenderRecord, type VideoState, artifactDirectory, beginArtifactCapture, finishSnapshotRun, forgetRecordedArtifacts, historyCache, listProjectHistories, pruneGoneProjectHistories, reclaimUncopiedBlobs, recordArtifact, recordArtifacts, recordRenderSuccess, registerRenderOutput, retainSnapshotOutput };
@@ -3,6 +3,7 @@ import {
3
3
  HistoryBusyError,
4
4
  HistoryClosedError,
5
5
  HistoryIdError,
6
+ HistoryRefusedError,
6
7
  MAX_WINDOW_IDLE_MS,
7
8
  START,
8
9
  UNDO_MODES,
@@ -15,12 +16,13 @@ import {
15
16
  openProjectHistory,
16
17
  pruneGoneProjectHistories,
17
18
  readProjectHistory,
19
+ reclaimUncopiedBlobs,
18
20
  recordArtifact,
19
21
  recordArtifacts,
20
22
  recordRenderSuccess,
21
23
  registerRenderOutput,
22
24
  retainSnapshotOutput
23
- } from "../chunk-YIMSBQIO.js";
25
+ } from "../chunk-JPKAFJIJ.js";
24
26
  import "../chunk-QF7MFJJU.js";
25
27
  import "../chunk-SVQFE44C.js";
26
28
  import "../chunk-L3HJVTQE.js";
@@ -30,6 +32,7 @@ export {
30
32
  HistoryBusyError,
31
33
  HistoryClosedError,
32
34
  HistoryIdError,
35
+ HistoryRefusedError,
33
36
  MAX_WINDOW_IDLE_MS,
34
37
  UNDO_MODES,
35
38
  artifactDirectory,
@@ -41,6 +44,7 @@ export {
41
44
  openProjectHistory,
42
45
  pruneGoneProjectHistories,
43
46
  readProjectHistory,
47
+ reclaimUncopiedBlobs,
44
48
  recordArtifact,
45
49
  recordArtifacts,
46
50
  recordRenderSuccess,
package/dist/index.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import { Hono } from 'hono';
2
- import { S as StudioApiAdapter, P as PreviewDocumentStore, M as MediaProcessingJobState } from './mediaProxyPreview-CAiGJucG.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-CAiGJucG.js';
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 HistoryTarget, g as HistoryWho, h as HistoryWindow, M as MAX_WINDOW_IDLE_MS, P as ProjectHistory, i as ProjectHistoryOptions, j as ProjectHistoryView, U as UNDO_MODES, k as UndoMode, o as openProjectHistory, r as readProjectHistory } from './projectHistory-ByC9sa3X.js';
5
- export { ArtifactCapture, ArtifactInput, ArtifactRecord, HistoryBusyError, HistoryIdError, ProjectHistoryRecord, PrunedHistory, RenderRecord, VideoState, artifactDirectory, beginArtifactCapture, finishSnapshotRun, forgetRecordedArtifacts, historyCache, listProjectHistories, pruneGoneProjectHistories, recordArtifact, recordArtifacts, recordRenderSuccess, registerRenderOutput, retainSnapshotOutput } from './history/index.js';
2
+ import { S as StudioApiAdapter, P as PreviewDocumentStore, M as MediaProcessingJobState } from './mediaProxyPreview-C0DZX2Mo.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-C0DZX2Mo.js';
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 HistoryRefusedError, f as HistoryResult, g as HistoryTarget, h as HistoryWho, i as HistoryWindow, M as MAX_WINDOW_IDLE_MS, P as ProjectHistory, j as ProjectHistoryOptions, k as ProjectHistoryView, U as UNDO_MODES, l as UndoMode, o as openProjectHistory, r as readProjectHistory } from './projectHistory-C18HDvac.js';
5
+ export { ArtifactCapture, ArtifactInput, ArtifactRecord, HistoryBusyError, HistoryIdError, ProjectHistoryRecord, PrunedHistory, ReclaimedHistory, RenderRecord, VideoState, artifactDirectory, beginArtifactCapture, finishSnapshotRun, forgetRecordedArtifacts, historyCache, listProjectHistories, pruneGoneProjectHistories, reclaimUncopiedBlobs, recordArtifact, recordArtifacts, recordRenderSuccess, registerRenderOutput, retainSnapshotOutput } from './history/index.js';
6
6
  export { CACHE_DIR_NAME as PROXY_CACHE_DIR_NAME } from './helpers/proxyTranscoder.js';
7
7
  export { walkDir } from './helpers/safePath.js';
8
8
  export { stampProjectHfIds } from './helpers/hfIdPersist.js';
package/dist/index.js CHANGED
@@ -31,6 +31,7 @@ import {
31
31
  HistoryBusyError,
32
32
  HistoryClosedError,
33
33
  HistoryIdError,
34
+ HistoryRefusedError,
34
35
  MAX_WINDOW_IDLE_MS,
35
36
  START,
36
37
  STUDIO_SIGNATURE_MANIFEST_PATHS,
@@ -51,6 +52,7 @@ import {
51
52
  openProjectHistory,
52
53
  pruneGoneProjectHistories,
53
54
  readProjectHistory,
55
+ reclaimUncopiedBlobs,
54
56
  recordArtifact,
55
57
  recordArtifacts,
56
58
  recordFileWriteReceipt,
@@ -60,7 +62,7 @@ import {
60
62
  resolveProjectSignature,
61
63
  retainSnapshotOutput,
62
64
  settledFileTag
63
- } from "./chunk-YIMSBQIO.js";
65
+ } from "./chunk-JPKAFJIJ.js";
64
66
  import {
65
67
  clearElementScreenshotIsolation,
66
68
  getElementScreenshotClip
@@ -74,7 +76,7 @@ import {
74
76
  requestedProjectPath,
75
77
  requiredFfmpeg,
76
78
  resolveLocalAudioPath
77
- } from "./chunk-ERQWOZ7E.js";
79
+ } from "./chunk-QZWVJKLI.js";
78
80
  import {
79
81
  injectMediaCodecMap,
80
82
  isAutoProxyEnabled,
@@ -6763,6 +6765,7 @@ export {
6763
6765
  HistoryBusyError,
6764
6766
  HistoryClosedError,
6765
6767
  HistoryIdError,
6768
+ HistoryRefusedError,
6766
6769
  MAX_UPLOAD_BYTES,
6767
6770
  MAX_WINDOW_IDLE_MS,
6768
6771
  MIME_TYPES,
@@ -6807,6 +6810,7 @@ export {
6807
6810
  patchElementInHtml,
6808
6811
  pruneGoneProjectHistories,
6809
6812
  readProjectHistory,
6813
+ reclaimUncopiedBlobs,
6810
6814
  recordArtifact,
6811
6815
  recordArtifacts,
6812
6816
  recordRenderSuccess,
@@ -3,7 +3,7 @@ import { RegistryItem, PreviewProxyBox } from '@hyperframes/core';
3
3
  import * as sharp from 'sharp';
4
4
  import { CanvasResolution } from '@hyperframes/parsers';
5
5
  import { BundleOptions } from '@hyperframes/core/compiler';
6
- import { P as ProjectHistory } from './projectHistory-ByC9sa3X.js';
6
+ import { P as ProjectHistory } from './projectHistory-C18HDvac.js';
7
7
  import { MediaCodecProbeCache, HtmlSourceLike } from './helpers/mediaCodecMap.js';
8
8
 
9
9
  /** Resolved info about a single project. */
@@ -146,6 +146,9 @@ interface ProjectHistory {
146
146
  declare class HistoryClosedError extends Error {
147
147
  constructor(message: string);
148
148
  }
149
+ declare class HistoryRefusedError extends Error {
150
+ constructor(message: string);
151
+ }
149
152
  /** Opens a project's history: every write to its files becomes an entry that can be undone or restored. */
150
153
  declare function openProjectHistory(options: ProjectHistoryOptions): Promise<ProjectHistory>;
151
154
  /** A project's history as its log stands now, read without the owner lock: it reads while another process owns it. */
@@ -157,4 +160,4 @@ interface ProjectHistoryView {
157
160
  /** Reads only: takes no lock and files, prunes or writes nothing; an owner's half-appended last line is skipped. */
158
161
  declare function readProjectHistory(options: Pick<ProjectHistoryOptions, "projectDir" | "historyRoot">): ProjectHistoryView;
159
162
 
160
- export { type ClosedWindow as C, DEFAULT_HISTORY_ROOT as D, HistoryClosedError as H, MAX_WINDOW_IDLE_MS as M, type ProjectHistory as P, START as S, UNDO_MODES as U, type HistoryEntry as a, type HistoryEntrySide as b, type HistoryFileChange as c, type HistoryListItem as d, type HistoryResult as e, type HistoryTarget as f, type HistoryWho as g, type HistoryWindow as h, type ProjectHistoryOptions as i, type ProjectHistoryView as j, type UndoMode as k, openProjectHistory as o, readProjectHistory as r };
163
+ export { type ClosedWindow as C, DEFAULT_HISTORY_ROOT as D, HistoryClosedError as H, MAX_WINDOW_IDLE_MS as M, type ProjectHistory as P, START as S, UNDO_MODES as U, type HistoryEntry as a, type HistoryEntrySide as b, type HistoryFileChange as c, type HistoryListItem as d, HistoryRefusedError as e, type HistoryResult as f, type HistoryTarget as g, type HistoryWho as h, type HistoryWindow as i, type ProjectHistoryOptions as j, type ProjectHistoryView as k, type UndoMode as l, openProjectHistory as o, readProjectHistory as r };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperframes/studio-server",
3
- "version": "0.8.191",
3
+ "version": "0.8.193",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/heygen-com/hyperframes",
@@ -84,8 +84,8 @@
84
84
  "linkedom": "^0.18.12",
85
85
  "postcss": "^8.5.8",
86
86
  "postcss-selector-parser": "^7.1.2",
87
- "@hyperframes/parsers": "0.8.191",
88
- "@hyperframes/core": "0.8.191"
87
+ "@hyperframes/core": "0.8.193",
88
+ "@hyperframes/parsers": "0.8.193"
89
89
  },
90
90
  "devDependencies": {
91
91
  "@types/node": "^25.0.10",