@hyperframes/studio-server 0.8.210 → 0.8.211

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.
@@ -19,7 +19,7 @@ import {
19
19
  rmSync as rmSync3
20
20
  } from "fs";
21
21
  import { homedir } from "os";
22
- import { dirname as dirname5, isAbsolute as isAbsolute5, join as join8, relative as relative5, resolve as resolve7, sep as sep5 } from "path";
22
+ import { dirname as dirname5, isAbsolute as isAbsolute5, join as join8, relative as relative5, resolve as resolve8, sep as sep5 } from "path";
23
23
  import { replaceFileAtomically as replaceFileAtomically6 } from "@hyperframes/core/atomic-file";
24
24
 
25
25
  // src/helpers/fileVersion.ts
@@ -534,11 +534,44 @@ function createProjectSignature(projectDir, excluding = /* @__PURE__ */ new Set(
534
534
  // src/history/blobStore.ts
535
535
  import { createHash as createHash3, randomUUID as randomUUID3 } from "crypto";
536
536
  import { constants, createReadStream, createWriteStream } from "fs";
537
- import { copyFile, mkdir, readdir, readFile, rm, stat } from "fs/promises";
537
+ import { copyFile, mkdir, open, readdir, readFile, rm, stat } from "fs/promises";
538
538
  import { dirname, join as join2 } from "path";
539
539
  import { pipeline } from "stream/promises";
540
540
  import { atomicTempPath } from "@hyperframes/core/atomic-file";
541
- import { renameWithRetry, renameWithRetryAsync } from "@hyperframes/core/rename-retry";
541
+ import {
542
+ isWindowsHold,
543
+ renameWithRetry,
544
+ renameWithRetryAsync
545
+ } from "@hyperframes/core/rename-retry";
546
+
547
+ // src/helpers/servedFiles.ts
548
+ import { resolve as resolve3 } from "path";
549
+ var serving = /* @__PURE__ */ new Map();
550
+ var keyOf = (path) => process.platform === "win32" ? resolve3(path).toLowerCase() : resolve3(path);
551
+ function serveWhileOpen(path, stream) {
552
+ const key = keyOf(path);
553
+ const reads = serving.get(key) ?? /* @__PURE__ */ new Set();
554
+ serving.set(key, reads.add(stream));
555
+ stream.once("close", () => {
556
+ reads.delete(stream);
557
+ if (reads.size === 0 && serving.get(key) === reads) serving.delete(key);
558
+ });
559
+ return stream;
560
+ }
561
+ async function stopServingAndClose(path) {
562
+ const reads = [...serving.get(keyOf(path)) ?? []];
563
+ await Promise.all(
564
+ reads.map(
565
+ (stream) => new Promise((done) => {
566
+ if (stream.closed) return done();
567
+ stream.once("close", () => done());
568
+ stream.destroy();
569
+ })
570
+ )
571
+ );
572
+ }
573
+
574
+ // src/history/blobStore.ts
542
575
  var BLOB_HASH = /^[0-9a-f]{64}$/;
543
576
  function blobPath(dir, hash) {
544
577
  if (!BLOB_HASH.test(hash)) throw new Error("That is not a history blob.");
@@ -558,18 +591,45 @@ async function cloneOrStream(from, to) {
558
591
  await pipeline(createReadStream(from), createWriteStream(to, { mode }));
559
592
  }
560
593
  }
561
- async function cloneOrCopy(from, to, beforeReplace) {
594
+ var inPlaceListeners = /* @__PURE__ */ new Set();
595
+ function onRestoredInPlace(listener) {
596
+ inPlaceListeners.add(listener);
597
+ return () => inPlaceListeners.delete(listener);
598
+ }
599
+ async function cloneOrCopy(from, to, hash, beforeReplace) {
562
600
  await mkdir(dirname(to), { recursive: true });
563
601
  const temp = atomicTempPath(to);
564
602
  try {
565
603
  await copyFile(from, temp, constants.COPYFILE_FICLONE);
604
+ await stopServingAndClose(to);
566
605
  beforeReplace?.();
567
- renameWithRetry(temp, to);
568
- } catch (error) {
606
+ try {
607
+ renameWithRetry(temp, to);
608
+ } catch (error) {
609
+ if (!isWindowsHold(error)) throw error;
610
+ await writeInPlace(temp, to, hash);
611
+ }
612
+ } finally {
569
613
  await rm(temp, { force: true });
570
- throw error;
571
614
  }
572
615
  }
616
+ async function writeInPlace(temp, to, hash) {
617
+ let outcome = "failed";
618
+ try {
619
+ await copyFile(temp, to);
620
+ const handle = await open(to, "r+");
621
+ try {
622
+ await handle.sync();
623
+ } finally {
624
+ await handle.close();
625
+ }
626
+ outcome = await hashFile(to) === hash ? "restored" : "mismatch";
627
+ } finally {
628
+ for (const listener of inPlaceListeners) listener(to, outcome);
629
+ }
630
+ if (outcome === "mismatch")
631
+ throw new Error("The file was written in place, but it does not match the copy history kept.");
632
+ }
573
633
  var listDir = (path) => readdir(path, { withFileTypes: true }).catch((error) => {
574
634
  if (error.code === "ENOENT") return [];
575
635
  throw error;
@@ -628,7 +688,7 @@ async function openBlobStore(dir) {
628
688
  reference: (absPath) => hashFile(absPath),
629
689
  has: (hash) => sizes.has(hash) || asideSizes.has(hash),
630
690
  read: (hash) => readFile(pathOf(hash)),
631
- writeTo: async (hash, absPath, beforeReplace) => cloneOrCopy(pathOf(hash), absPath, beforeReplace),
691
+ writeTo: async (hash, absPath, beforeReplace) => cloneOrCopy(pathOf(hash), absPath, hash, beforeReplace),
632
692
  async release(held) {
633
693
  for (const [hash, path] of held) {
634
694
  holds.get(hash)?.delete(path);
@@ -717,7 +777,7 @@ import { dirname as dirname2, join as join4 } from "path";
717
777
  // src/history/historyId.ts
718
778
  import { randomUUID as randomUUID4 } from "crypto";
719
779
  import { existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSync4, statSync as statSync2, writeFileSync as writeFileSync2 } from "fs";
720
- import { basename, join as join3, resolve as resolve3 } from "path";
780
+ import { basename, join as join3, resolve as resolve4 } from "path";
721
781
  import { replaceFileAtomically as replaceFileAtomically3 } from "@hyperframes/core/atomic-file";
722
782
  var ID_PATH = join3(".hyperframes", "history-id");
723
783
  var ID_SHAPE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
@@ -756,7 +816,7 @@ function isCopyOf0878Folder(recordedDir, id, folder) {
756
816
  return !!there && !sameFolder(there, folder) && readId(recordedDir) === id;
757
817
  }
758
818
  function projectHistoryId(projectDir, historyRoot) {
759
- const dir = resolve3(projectDir);
819
+ const dir = resolve4(projectDir);
760
820
  const folder = statSync2(dir);
761
821
  let id = readId(dir);
762
822
  if (!id && existsSync(join3(dir, ID_PATH)))
@@ -933,7 +993,7 @@ function pruneGoneProjectHistoriesDaily(historyRoot, now, { onError, budgetMs }
933
993
  // src/history/videoState.ts
934
994
  import { createHash as createHash5, randomUUID as randomUUID6 } from "crypto";
935
995
  import { createReadStream as createReadStream2, existsSync as existsSync2, readFileSync as readFileSync8, renameSync as renameSync2, statSync as statSync4 } from "fs";
936
- import { isAbsolute as isAbsolute4, join as join7, resolve as resolve6 } from "path";
996
+ import { isAbsolute as isAbsolute4, join as join7, resolve as resolve7 } from "path";
937
997
  import { replaceFileAtomically as replaceFileAtomically4 } from "@hyperframes/core/atomic-file";
938
998
 
939
999
  // src/history/artifactCleanup.ts
@@ -947,7 +1007,7 @@ import {
947
1007
  readFileSync as readFileSync6,
948
1008
  unlinkSync
949
1009
  } from "fs";
950
- import { isAbsolute as isAbsolute3, join as join5, relative as relative3, resolve as resolve4, sep as sep3 } from "path";
1010
+ import { isAbsolute as isAbsolute3, join as join5, relative as relative3, resolve as resolve5, sep as sep3 } from "path";
951
1011
  var kinds = ["checks", "snapshots"];
952
1012
  var records = (state, kind) => [
953
1013
  ...Object.values(state[kind].revisions).flat(),
@@ -955,7 +1015,7 @@ var records = (state, kind) => [
955
1015
  ];
956
1016
  var protectedRevisions = (state) => /* @__PURE__ */ new Set([state.currentRevision, ...state.renders?.retainedRevisions ?? []]);
957
1017
  function managedPath(projectDir, path, kind) {
958
- const canonicalProject = realFilePath(resolve4(projectDir));
1018
+ const canonicalProject = realFilePath(resolve5(projectDir));
959
1019
  const canonicalPath = realFilePath(path);
960
1020
  const root = join5(canonicalProject, ".hyperframes", "artifacts", kind);
961
1021
  const rel = relative3(root, canonicalPath);
@@ -1067,15 +1127,15 @@ import {
1067
1127
  writeSync,
1068
1128
  writeFileSync as writeFileSync4
1069
1129
  } from "fs";
1070
- import { dirname as dirname3, join as join6, relative as relative4, resolve as resolve5, sep as sep4 } from "path";
1130
+ import { dirname as dirname3, join as join6, relative as relative4, resolve as resolve6, sep as sep4 } from "path";
1071
1131
  var SNAPSHOT_RUNS_TO_KEEP = 10;
1072
1132
  var SNAPSHOT_GRACE_MS = 24 * 60 * 60 * 1e3;
1073
1133
  var OWNER = ".capture-owner.json";
1074
1134
  var ownedMarkers = /* @__PURE__ */ new Map();
1075
1135
  var UUID = /^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$/i;
1076
- var rootOf = (project) => join6(resolve5(project), ".hyperframes", "artifacts", "snapshots");
1136
+ var rootOf = (project) => join6(resolve6(project), ".hyperframes", "artifacts", "snapshots");
1077
1137
  function ordinaryPath(project, path) {
1078
- const root = resolve5(project);
1138
+ const root = resolve6(project);
1079
1139
  const rel = relative4(root, path);
1080
1140
  if (rel === ".." || rel.startsWith(`..${sep4}`)) return false;
1081
1141
  let current = root;
@@ -1090,7 +1150,7 @@ function ordinaryPath(project, path) {
1090
1150
  return true;
1091
1151
  }
1092
1152
  function createSnapshotParents(project, path) {
1093
- let parent = resolve5(project);
1153
+ let parent = resolve6(project);
1094
1154
  for (const part of relative4(parent, path).split(sep4).filter(Boolean)) {
1095
1155
  parent = join6(parent, part);
1096
1156
  try {
@@ -1186,7 +1246,7 @@ function recordsOf(state) {
1186
1246
  }
1187
1247
  function containingRun(root, path) {
1188
1248
  if (!path) return null;
1189
- const name = relative4(root, resolve5(path)).split(sep4)[0];
1249
+ const name = relative4(root, resolve6(path)).split(sep4)[0];
1190
1250
  return name && UUID.test(name) ? join6(root, name) : null;
1191
1251
  }
1192
1252
  function keepContainingRun(keep, root, path) {
@@ -1287,7 +1347,7 @@ function retainSnapshotOutput(project, path) {
1287
1347
  const root = rootOf(project);
1288
1348
  const run = containingRun(root, path);
1289
1349
  if (!run) return;
1290
- createSnapshotParents(project, resolve5(path));
1350
+ createSnapshotParents(project, resolve6(path));
1291
1351
  try {
1292
1352
  writeFileSync4(
1293
1353
  join6(run, ".user-output"),
@@ -1343,7 +1403,7 @@ async function updateState(dir, update) {
1343
1403
  }
1344
1404
  }
1345
1405
  async function writeHistoryState(projectDir, historyId, historyDirectory, entries, currentFiles, committedFiles) {
1346
- const blob = (hash) => hash === null ? null : resolve6(historyDirectory, "blobs", hash.slice(0, 2), hash);
1406
+ const blob = (hash) => hash === null ? null : resolve7(historyDirectory, "blobs", hash.slice(0, 2), hash);
1347
1407
  await updateState(projectDir, (state) => {
1348
1408
  if (state.historyId && state.historyId !== historyId) {
1349
1409
  state.checks = collection();
@@ -1474,7 +1534,7 @@ function listsOf(artifacts) {
1474
1534
  return [...Object.values(artifacts.revisions), artifacts.unversioned];
1475
1535
  }
1476
1536
  async function forgetRecordedArtifacts(projectDir, paths) {
1477
- const gone = new Set(paths.map((path) => resolve6(path)));
1537
+ const gone = new Set(paths.map((path) => resolve7(path)));
1478
1538
  if (gone.size === 0) return;
1479
1539
  await updateState(projectDir, (state) => {
1480
1540
  for (const artifacts of [state.checks, state.snapshots]) {
@@ -1482,7 +1542,7 @@ async function forgetRecordedArtifacts(projectDir, paths) {
1482
1542
  for (let i = list.length - 1; i >= 0; i--) {
1483
1543
  const record = list[i];
1484
1544
  const path = record.image ?? record.report;
1485
- if (path && gone.has(resolve6(path))) list.splice(i, 1);
1545
+ if (path && gone.has(resolve7(path))) list.splice(i, 1);
1486
1546
  }
1487
1547
  }
1488
1548
  if (artifacts.latest && !listsOf(artifacts).some(
@@ -1494,7 +1554,7 @@ async function forgetRecordedArtifacts(projectDir, paths) {
1494
1554
  });
1495
1555
  }
1496
1556
  function artifactDirectory(projectDir, kind) {
1497
- const path = join7(resolve6(projectDir), ".hyperframes", "artifacts", kind, randomUUID6());
1557
+ const path = join7(resolve7(projectDir), ".hyperframes", "artifacts", kind, randomUUID6());
1498
1558
  if (kind === "snapshots") ownSnapshotRun(projectDir, path);
1499
1559
  return path;
1500
1560
  }
@@ -1510,7 +1570,7 @@ async function recordRenderSuccess(projectDir, output, status = "completed", cap
1510
1570
  const record = {
1511
1571
  id: randomUUID6(),
1512
1572
  completedAt: Date.now(),
1513
- output: resolve6(output),
1573
+ output: resolve7(output),
1514
1574
  status
1515
1575
  };
1516
1576
  const unchanged = Boolean(capture && after?.fingerprint === capture.fingerprint);
@@ -1777,7 +1837,7 @@ var Engine = class {
1777
1837
  this.options = options;
1778
1838
  this.projectId = projectId;
1779
1839
  this.blobs = blobs;
1780
- this.dir = resolve7(options.projectDir);
1840
+ this.dir = resolve8(options.projectDir);
1781
1841
  this.home = join8(options.historyRoot, projectId);
1782
1842
  this.foldsCase = ignoresCase(this.dir);
1783
1843
  const folder = statSync5(this.dir, { throwIfNoEntry: false });
@@ -2086,7 +2146,7 @@ var Engine = class {
2086
2146
  return known.hash !== hash || known.stat !== stat4;
2087
2147
  }
2088
2148
  async record(path, before, after, at) {
2089
- const endedBeforeThisWrite = this.windows.filter((open) => !takesWrite(open, at));
2149
+ const endedBeforeThisWrite = this.windows.filter((open2) => !takesWrite(open2, at));
2090
2150
  if (endedBeforeThisWrite.length) await this.commitOutside();
2091
2151
  for (const window2 of endedBeforeThisWrite) await this.endWindow(window2);
2092
2152
  const window = this.windows.at(-1);
@@ -2128,7 +2188,7 @@ var Engine = class {
2128
2188
  if (window.changes.size) await this.commitCut(window, [...window.changes.keys()]);
2129
2189
  }
2130
2190
  pendingElsewhere() {
2131
- return Boolean(this.outside?.changes.size) || this.windows.some((open) => open.changes.size > 0);
2191
+ return Boolean(this.outside?.changes.size) || this.windows.some((open2) => open2.changes.size > 0);
2132
2192
  }
2133
2193
  /** The held claim `taken` continues: same key, each file from its own last change, no held file changed since. */
2134
2194
  heldFor(key, taken) {
@@ -2142,7 +2202,7 @@ var Engine = class {
2142
2202
  return group && taken.every((change) => after(change.path, change.before) === change.before) && [...group.changes.values()].every(untouched) ? group : null;
2143
2203
  }
2144
2204
  logPath(path) {
2145
- return relative5(this.dir, resolve7(this.dir, path)).split(sep5).join("/");
2205
+ return relative5(this.dir, resolve8(this.dir, path)).split(sep5).join("/");
2146
2206
  }
2147
2207
  /** Takes `paths`' uncommitted changes from others, cut at what `who` overwrote. */
2148
2208
  async takeClaimed(who, paths, overwrote) {
@@ -2153,7 +2213,7 @@ var Engine = class {
2153
2213
  hashOfVersion(version)
2154
2214
  ])
2155
2215
  );
2156
- const others = this.windows.filter((open) => !sameWho(open.who, who));
2216
+ const others = this.windows.filter((open2) => !sameWho(open2.who, who));
2157
2217
  const groups = this.outside ? [this.outside, ...others] : others;
2158
2218
  const hits = groups.flatMap(
2159
2219
  (group) => [...group.changes.values()].filter((change) => wanted.has(change.path)).map((change) => ({ group, change }))
@@ -2303,7 +2363,7 @@ var Engine = class {
2303
2363
  }
2304
2364
  /** A watcher saw a write: one sweep per burst takes it in (a deleted folder is reported by its name alone). */
2305
2365
  noteChange(path) {
2306
- const absolute = resolve7(this.dir, path);
2366
+ const absolute = resolve8(this.dir, path);
2307
2367
  if (this.notedTimer || !affectsProjectSignature(this.dir, absolute)) return;
2308
2368
  if (!isHistoryPath(relative5(this.dir, absolute).split(sep5).join("/"))) return;
2309
2369
  this.notedTimer = setTimeout(() => {
@@ -2323,12 +2383,12 @@ var Engine = class {
2323
2383
  const at = this.now();
2324
2384
  if (exclusive) {
2325
2385
  const blocking = this.windows.find(
2326
- (open) => !sameWho(open.who, who) && takesWrite(open, at)
2386
+ (open2) => !sameWho(open2.who, who) && takesWrite(open2, at)
2327
2387
  );
2328
2388
  if (blocking)
2329
2389
  throw new Error(`${blocking.who.name}'s turn is open; it has to end before yours begins`);
2330
- for (const open of [...this.windows])
2331
- if (!takesWrite(open, at) || sameWho(open.who, who)) await this.endWindow(open);
2390
+ for (const open2 of [...this.windows])
2391
+ if (!takesWrite(open2, at) || sameWho(open2.who, who)) await this.endWindow(open2);
2332
2392
  }
2333
2393
  const window = { ...this.newGroup(who, label), idleMs };
2334
2394
  this.windows.push(window);
@@ -2365,7 +2425,7 @@ var Engine = class {
2365
2425
  if (!this.windows.includes(window)) return window.entry ?? null;
2366
2426
  clearTimeout(window.idleTimer);
2367
2427
  await this.commitClaim();
2368
- this.windows = this.windows.filter((open) => open !== window);
2428
+ this.windows = this.windows.filter((open2) => open2 !== window);
2369
2429
  window.entry = await this.commit(window) ?? window.entry ?? null;
2370
2430
  return window.entry;
2371
2431
  }
@@ -2424,7 +2484,7 @@ var Engine = class {
2424
2484
  );
2425
2485
  await this.sweep();
2426
2486
  } finally {
2427
- this.windows = this.windows.filter((open) => open !== group);
2487
+ this.windows = this.windows.filter((open2) => open2 !== group);
2428
2488
  }
2429
2489
  return this.commit(group, extra);
2430
2490
  }
@@ -2491,7 +2551,7 @@ var Engine = class {
2491
2551
  );
2492
2552
  if (pending) return direction === "back" ? this.pendingEntry(pending) : void 0;
2493
2553
  const everyone = this.options.undoScope === "everyone";
2494
- const ofOpenTurn = (entry) => this.windows.some((open) => open.parts?.has(entry.id));
2554
+ const ofOpenTurn = (entry) => this.windows.some((open2) => open2.parts?.has(entry.id));
2495
2555
  return stepTarget(
2496
2556
  this.log.entries,
2497
2557
  direction,
@@ -2590,7 +2650,7 @@ var Engine = class {
2590
2650
  checkout: (entryId, side, emptyDir) => this.queue(async () => {
2591
2651
  this.entry(entryId);
2592
2652
  if (!existsSync3(this.dir)) throw new Error(`The project folder is gone: ${this.dir}`);
2593
- const dest = resolve7(emptyDir);
2653
+ const dest = resolve8(emptyDir);
2594
2654
  if (isWithin(this.dir, dest))
2595
2655
  throw new Error(`Checkout writes outside the project only: ${dest}`);
2596
2656
  if ((await readdir3(dest).catch(missingIsEmpty)).length > 0)
@@ -2641,7 +2701,7 @@ var Engine = class {
2641
2701
  }
2642
2702
  };
2643
2703
  function isWithin(dir, path) {
2644
- let probe = resolve7(path);
2704
+ let probe = resolve8(path);
2645
2705
  while (!existsSync3(probe) && dirname5(probe) !== probe) probe = dirname5(probe);
2646
2706
  const rel = relative5(realpathSync.native(dir), realpathSync.native(probe));
2647
2707
  return rel === "" || rel !== ".." && !rel.startsWith(`..${sep5}`) && !isAbsolute5(rel);
@@ -2694,7 +2754,7 @@ async function openProjectHistory(options) {
2694
2754
  }
2695
2755
  }
2696
2756
  function readProjectHistory(options) {
2697
- const dir = resolve7(options.projectDir);
2757
+ const dir = resolve8(options.projectDir);
2698
2758
  const id = readId(dir);
2699
2759
  const home = id && isRecordedFolder(join8(options.historyRoot, id), statSync5(dir)) ? join8(options.historyRoot, id) : null;
2700
2760
  const read = home && readLog(logFileIn(home), () => {
@@ -2771,14 +2831,14 @@ async function reclaimUncopiedBlobs(historyRoot, { dryRun = false, onError } = {
2771
2831
  }
2772
2832
 
2773
2833
  // src/history/historyCache.ts
2774
- function historyCache(open) {
2834
+ function historyCache(open2) {
2775
2835
  const histories = /* @__PURE__ */ new Map();
2776
2836
  const get = async (projectDir) => {
2777
2837
  let cached = histories.get(projectDir);
2778
- if (!cached) histories.set(projectDir, cached = open(projectDir));
2838
+ if (!cached) histories.set(projectDir, cached = open2(projectDir));
2779
2839
  const opened = await cached;
2780
2840
  if (!opened?.replacedAtPath()) return opened;
2781
- const reopen = () => open(projectDir);
2841
+ const reopen = () => open2(projectDir);
2782
2842
  if (histories.get(projectDir) === cached)
2783
2843
  histories.set(projectDir, opened.close().then(reopen, reopen));
2784
2844
  return get(projectDir);
@@ -2810,6 +2870,8 @@ export {
2810
2870
  clearFileWriteReceipt,
2811
2871
  identifyFileWrite,
2812
2872
  consumeFileWriteReceipt,
2873
+ serveWhileOpen,
2874
+ onRestoredInPlace,
2813
2875
  HistoryIdError,
2814
2876
  listProjectHistories,
2815
2877
  pruneGoneProjectHistories,
@@ -17,6 +17,10 @@ declare class HistoryBusyError extends Error {
17
17
  constructor(pid: number, url?: string | undefined);
18
18
  }
19
19
 
20
+ type InPlaceOutcome = "restored" | "mismatch" | "failed";
21
+ type InPlaceListener = (absPath: string, outcome: InPlaceOutcome) => void;
22
+ declare function onRestoredInPlace(listener: InPlaceListener): () => void;
23
+
20
24
  declare class HistoryIdError extends Error {
21
25
  constructor(message: string);
22
26
  }
@@ -148,4 +152,4 @@ declare function registerRenderOutput(projectDir: string, output: string): Promi
148
152
  /** Explicit outputs remain user evidence even when reusing a managed UUID folder. */
149
153
  declare function retainSnapshotOutput(project: string, path: string): void;
150
154
 
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 };
155
+ export { type ArtifactCapture, type ArtifactInput, type ArtifactRecord, HistoryBusyError, HistoryIdError, HistoryTarget, HistoryWho, type InPlaceOutcome, ProjectHistory, type ProjectHistoryRecord, type PrunedHistory, type ReclaimedHistory, type RenderRecord, type VideoState, artifactDirectory, beginArtifactCapture, finishSnapshotRun, forgetRecordedArtifacts, historyCache, listProjectHistories, onRestoredInPlace, pruneGoneProjectHistories, reclaimUncopiedBlobs, recordArtifact, recordArtifacts, recordRenderSuccess, registerRenderOutput, retainSnapshotOutput };
@@ -13,6 +13,7 @@ import {
13
13
  forgetRecordedArtifacts,
14
14
  historyCache,
15
15
  listProjectHistories,
16
+ onRestoredInPlace,
16
17
  openProjectHistory,
17
18
  pruneGoneProjectHistories,
18
19
  readProjectHistory,
@@ -22,7 +23,7 @@ import {
22
23
  recordRenderSuccess,
23
24
  registerRenderOutput,
24
25
  retainSnapshotOutput
25
- } from "../chunk-BWZMWYB2.js";
26
+ } from "../chunk-UMPRW7BV.js";
26
27
  import "../chunk-QF7MFJJU.js";
27
28
  import "../chunk-SVQFE44C.js";
28
29
  import "../chunk-L3HJVTQE.js";
@@ -41,6 +42,7 @@ export {
41
42
  forgetRecordedArtifacts,
42
43
  historyCache,
43
44
  listProjectHistories,
45
+ onRestoredInPlace,
44
46
  openProjectHistory,
45
47
  pruneGoneProjectHistories,
46
48
  readProjectHistory,
package/dist/index.d.ts CHANGED
@@ -2,7 +2,7 @@ import { Hono } from 'hono';
2
2
  import { S as StudioApiAdapter, P as PreviewDocumentStore, M as MediaProcessingJobState } from './mediaProxyPreview-DIEtpLJo.js';
3
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-DIEtpLJo.js';
4
4
  export { C as ClosedWindow, D as DEFAULT_HISTORY_ROOT, S as HISTORY_START, H as HistoryClosedError, a as HistoryEntry, b as HistoryEntrySide, c as HistoryFileChange, d as HistoryListItem, e as 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';
5
+ export { ArtifactCapture, ArtifactInput, ArtifactRecord, HistoryBusyError, HistoryIdError, InPlaceOutcome, ProjectHistoryRecord, PrunedHistory, ReclaimedHistory, RenderRecord, VideoState, artifactDirectory, beginArtifactCapture, finishSnapshotRun, forgetRecordedArtifacts, historyCache, listProjectHistories, onRestoredInPlace, 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
@@ -49,6 +49,7 @@ import {
49
49
  historyCache,
50
50
  identifyFileWrite,
51
51
  listProjectHistories,
52
+ onRestoredInPlace,
52
53
  openProjectHistory,
53
54
  pruneGoneProjectHistories,
54
55
  readProjectHistory,
@@ -61,8 +62,9 @@ import {
61
62
  resolveProjectAndSignature,
62
63
  resolveProjectSignature,
63
64
  retainSnapshotOutput,
65
+ serveWhileOpen,
64
66
  settledFileTag
65
- } from "./chunk-BWZMWYB2.js";
67
+ } from "./chunk-UMPRW7BV.js";
66
68
  import {
67
69
  clearElementScreenshotIsolation,
68
70
  getElementScreenshotClip
@@ -4664,7 +4666,7 @@ ${etag}`;
4664
4666
  // Node's web stream type and the DOM one do not overlap for tsc on
4665
4667
  // every platform's lib set; the double cast is the documented bridge.
4666
4668
  Readable.toWeb(
4667
- createReadStream(servedPath, { start, end })
4669
+ serveWhileOpen(servedPath, createReadStream(servedPath, { start, end }))
4668
4670
  )
4669
4671
  );
4670
4672
  const rangeHeader = c.req.header("Range");
@@ -7012,6 +7014,7 @@ export {
7012
7014
  identifyFileWrite,
7013
7015
  isSafePath,
7014
7016
  listProjectHistories,
7017
+ onRestoredInPlace,
7015
7018
  openProjectHistory,
7016
7019
  parseSourceDocument,
7017
7020
  patchElementInHtml,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperframes/studio-server",
3
- "version": "0.8.210",
3
+ "version": "0.8.211",
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/core": "0.8.210",
88
- "@hyperframes/parsers": "0.8.210"
87
+ "@hyperframes/core": "0.8.211",
88
+ "@hyperframes/parsers": "0.8.211"
89
89
  },
90
90
  "devDependencies": {
91
91
  "@types/node": "^25.0.10",