@hyperframes/studio-server 0.8.76 → 0.8.77

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.
@@ -12,6 +12,7 @@ import {
12
12
 
13
13
  // src/helpers/mediaProxyPreview.ts
14
14
  import { resolve } from "path";
15
+ import { insertBeforeCloseTag } from "@hyperframes/core/compiler/html-document";
15
16
  function isAutoProxyEnabled(adapter) {
16
17
  return adapter.autoProxy !== false;
17
18
  }
@@ -22,12 +23,6 @@ function proxyEtagSalt(raw) {
22
23
  if (raw === void 0) return "";
23
24
  return `:proxy:${raw}:${PROXY_PARAMS_VERSION}`;
24
25
  }
25
- function injectScriptTagIntoHead(html, scriptTag) {
26
- if (html.includes("</head>")) return html.replace("</head>", `${scriptTag}
27
- </head>`);
28
- return `${scriptTag}
29
- ${html}`;
30
- }
31
26
  async function injectMediaCodecMapIntoHtml(html, projectDir, htmlSources, probeCache) {
32
27
  let map;
33
28
  try {
@@ -52,7 +47,9 @@ async function injectMediaCodecMapIntoHtml(html, projectDir, htmlSources, probeC
52
47
  }
53
48
  const json = JSON.stringify(map).replace(/</g, "\\u003c").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
54
49
  const tag = `<script data-hf-media-codec-map>window.__HF_MEDIA_CODEC_MAP__=${json};</script>`;
55
- return injectScriptTagIntoHead(html, tag);
50
+ return insertBeforeCloseTag(html, "head", `${tag}
51
+ `) ?? `${tag}
52
+ ${html}`;
56
53
  }
57
54
  async function injectMediaCodecMap(html, adapter, projectDir, compSrcPath, probeCache) {
58
55
  if (!isAutoProxyEnabled(adapter)) return html;
@@ -66,4 +63,4 @@ export {
66
63
  injectMediaCodecMapIntoHtml,
67
64
  injectMediaCodecMap
68
65
  };
69
- //# sourceMappingURL=chunk-5PGWTBCL.js.map
66
+ //# sourceMappingURL=chunk-X3T65KC2.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/helpers/mediaProxyPreview.ts"],"sourcesContent":["import { resolve } from \"node:path\";\nimport { insertBeforeCloseTag } from \"@hyperframes/core/compiler/html-document\";\nimport type { StudioApiAdapter } from \"../types.js\";\nimport {\n createMediaCodecProbeCache,\n proxyVariantFor,\n recordProxyPrewarm,\n scanProjectMediaCodecMap,\n shouldPrewarmProxy,\n type HtmlSourceLike,\n type MediaCodecMap,\n type MediaCodecProbeCache,\n} from \"./mediaCodecMap.js\";\nimport { resolveProxy, PROXY_PARAMS_VERSION } from \"./proxyTranscoder.js\";\n\n/**\n * Transparent-media-proxy wiring shared by `routes/preview.ts`\n * (docs/plans/2026-07-14-002-feat-transparent-media-proxies-plan.md, unit U3).\n * Split out of the route module to keep it under the repo's 600-line file cap.\n */\n\n/**\n * Preview-route-local adapter surface for the auto-proxy feature. Both\n * fields are optional so any existing `StudioApiAdapter` value remains\n * structurally assignable without editing the shared interface:\n * `autoProxy` defaults to true (on) when omitted — a later unit wires the\n * CLI `--no-proxy` flag / `hyperframes.json` setting through it;\n * `mediaCodecProbeCache` lets a host share one probe cache across\n * preview/play/static-server surfaces instead of each constructing its own.\n */\nexport type PreviewApiAdapter = StudioApiAdapter & {\n autoProxy?: boolean;\n mediaCodecProbeCache?: MediaCodecProbeCache;\n /** Keeps built preview documents across restarts, keyed by project id and preview ETag. */\n previewDocuments?: PreviewDocumentStore;\n};\n\nexport interface PreviewDocumentStore {\n read(key: string): string | null;\n write(key: string, html: string): void;\n}\n\nexport function isAutoProxyEnabled(adapter: PreviewApiAdapter): boolean {\n return adapter.autoProxy !== false;\n}\n\n/** One probe cache per server instance — construct once in `registerPreviewRoutes`\n * and reuse across every request so the mtime-cache benefit in\n * `scanProjectMediaCodecMap` actually applies. A host that wants to share the\n * cache across other surfaces (play, static project server) can pass its own\n * via `adapter.mediaCodecProbeCache`. */\nexport function resolvePreviewMediaCodecProbeCache(\n adapter: PreviewApiAdapter,\n): MediaCodecProbeCache {\n return adapter.mediaCodecProbeCache ?? createMediaCodecProbeCache();\n}\n\n/**\n * ETag salt for `?hf-proxy=` asset requests, mirroring `variablesEtagSalt` in\n * preview.ts: salted by the raw param value plus the transcoder's params\n * version, so a future proxy-recipe change (which bumps `PROXY_PARAMS_VERSION`)\n * or a different proxy variant invalidates cached 304s without needing to\n * touch the proxy file itself.\n */\nexport function proxyEtagSalt(raw: string | undefined): string {\n if (raw === undefined) return \"\";\n return `:proxy:${raw}:${PROXY_PARAMS_VERSION}`;\n}\n\n/**\n * Injects `window.__HF_MEDIA_CODEC_MAP__` (the U1 codec-facts scan) into\n * served composition HTML, and fire-and-forget pre-warms `resolveProxy` for\n * every entry whose codec no browser decodes, so an element's proactive swap\n * usually hits a warm cache (KTD: protects the per-origin connection budget\n * under held responses). Conditionally hostile codecs are injected but NOT\n * pre-warmed: the requesting browser usually plays them, and the transcode\n * runs concurrently with its first layout. No second concurrency limiter here\n * — the transcoder's own global bound throttles both pre-warm and\n * element-triggered calls. Pre-warm failures are swallowed; an actual\n * `?hf-proxy=` request surfaces them as a 502 and transcodes lazily.\n * Alpha-bearing entries pre-warm their VP8/WebM variant.\n *\n * The single shared implementation for every auto-proxy surface — the studio\n * preview route (via `injectMediaCodecMap` below) and the CLI's composition /\n * static project servers (via the `./media-proxy-preview` subpath export).\n * Empty maps leave HTML untouched, preserving the normal no-hostile-media\n * preview path. On-demand proxy requests enforce the same eligibility gate.\n */\nexport async function injectMediaCodecMapIntoHtml(\n html: string,\n projectDir: string,\n htmlSources: HtmlSourceLike[],\n probeCache?: MediaCodecProbeCache,\n): Promise<string> {\n let map: MediaCodecMap;\n try {\n map = await scanProjectMediaCodecMap(\n projectDir,\n htmlSources,\n probeCache ? { cache: probeCache } : {},\n );\n } catch {\n // Best-effort: a scan failure must never block serving the page.\n return html;\n }\n if (Object.keys(map).length === 0) return html;\n for (const [rootRelativePathname, facts] of Object.entries(map)) {\n if (!shouldPrewarmProxy(facts)) continue;\n recordProxyPrewarm();\n resolveProxy(\n projectDir,\n resolve(projectDir, rootRelativePathname.replace(/^\\/+/, \"\")),\n proxyVariantFor(facts),\n ).catch(() => {\n // Swallowed: the pre-warm is best-effort. A real `?hf-proxy=` request\n // for this asset re-attempts the transcode and reports failure (502).\n });\n }\n // <-escape prevents a src path containing \"</script>\" from breaking out of\n // the injected tag, mirroring injectPreviewVariables in helpers/previewVariables.ts.\n const json = JSON.stringify(map)\n .replace(/</g, \"\\\\u003c\")\n .replace(/\\u2028/g, \"\\\\u2028\")\n .replace(/\\u2029/g, \"\\\\u2029\");\n const tag = `<script data-hf-media-codec-map>window.__HF_MEDIA_CODEC_MAP__=${json};</script>`;\n return insertBeforeCloseTag(html, \"head\", `${tag}\\n`) ?? `${tag}\\n${html}`;\n}\n\n/**\n * Adapter-aware wrapper used by the studio preview routes: skipped entirely\n * (no scan, no injection) when auto-proxy is off for this adapter.\n */\nexport async function injectMediaCodecMap(\n html: string,\n adapter: PreviewApiAdapter,\n projectDir: string,\n compSrcPath: string,\n probeCache: MediaCodecProbeCache,\n): Promise<string> {\n if (!isAutoProxyEnabled(adapter)) return html;\n return injectMediaCodecMapIntoHtml(html, projectDir, [{ html, compSrcPath }], probeCache);\n}\n"],"mappings":";;;;;;;;;;;;;AAAA,SAAS,eAAe;AACxB,SAAS,4BAA4B;AAyC9B,SAAS,mBAAmB,SAAqC;AACtE,SAAO,QAAQ,cAAc;AAC/B;AAOO,SAAS,mCACd,SACsB;AACtB,SAAO,QAAQ,wBAAwB,2BAA2B;AACpE;AASO,SAAS,cAAc,KAAiC;AAC7D,MAAI,QAAQ,OAAW,QAAO;AAC9B,SAAO,UAAU,GAAG,IAAI,oBAAoB;AAC9C;AAqBA,eAAsB,4BACpB,MACA,YACA,aACA,YACiB;AACjB,MAAI;AACJ,MAAI;AACF,UAAM,MAAM;AAAA,MACV;AAAA,MACA;AAAA,MACA,aAAa,EAAE,OAAO,WAAW,IAAI,CAAC;AAAA,IACxC;AAAA,EACF,QAAQ;AAEN,WAAO;AAAA,EACT;AACA,MAAI,OAAO,KAAK,GAAG,EAAE,WAAW,EAAG,QAAO;AAC1C,aAAW,CAAC,sBAAsB,KAAK,KAAK,OAAO,QAAQ,GAAG,GAAG;AAC/D,QAAI,CAAC,mBAAmB,KAAK,EAAG;AAChC,uBAAmB;AACnB;AAAA,MACE;AAAA,MACA,QAAQ,YAAY,qBAAqB,QAAQ,QAAQ,EAAE,CAAC;AAAA,MAC5D,gBAAgB,KAAK;AAAA,IACvB,EAAE,MAAM,MAAM;AAAA,IAGd,CAAC;AAAA,EACH;AAGA,QAAM,OAAO,KAAK,UAAU,GAAG,EAC5B,QAAQ,MAAM,SAAS,EACvB,QAAQ,WAAW,SAAS,EAC5B,QAAQ,WAAW,SAAS;AAC/B,QAAM,MAAM,iEAAiE,IAAI;AACjF,SAAO,qBAAqB,MAAM,QAAQ,GAAG,GAAG;AAAA,CAAI,KAAK,GAAG,GAAG;AAAA,EAAK,IAAI;AAC1E;AAMA,eAAsB,oBACpB,MACA,SACA,YACA,aACA,YACiB;AACjB,MAAI,CAAC,mBAAmB,OAAO,EAAG,QAAO;AACzC,SAAO,4BAA4B,MAAM,YAAY,CAAC,EAAE,MAAM,YAAY,CAAC,GAAG,UAAU;AAC1F;","names":[]}
@@ -1,4 +1,4 @@
1
- export { g as PreviewApiAdapter, P as PreviewDocumentStore, n as injectMediaCodecMap, p as injectMediaCodecMapIntoHtml, q as isAutoProxyEnabled, r as proxyEtagSalt, s as resolvePreviewMediaCodecProbeCache } from '../mediaProxyPreview-DZD3iu_2.js';
1
+ export { g as PreviewApiAdapter, P as PreviewDocumentStore, n as injectMediaCodecMap, p as injectMediaCodecMapIntoHtml, q as isAutoProxyEnabled, r as proxyEtagSalt, s as resolvePreviewMediaCodecProbeCache } from '../mediaProxyPreview-CIayJ4Y4.js';
2
2
  import './mediaCodecMap.js';
3
3
  import '@hyperframes/core';
4
4
  import '@hyperframes/parsers';
@@ -4,7 +4,7 @@ import {
4
4
  isAutoProxyEnabled,
5
5
  proxyEtagSalt,
6
6
  resolvePreviewMediaCodecProbeCache
7
- } from "../chunk-5PGWTBCL.js";
7
+ } from "../chunk-X3T65KC2.js";
8
8
  import "../chunk-45POWYFS.js";
9
9
  import "../chunk-4MNTZVVZ.js";
10
10
  export {
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Hono } from 'hono';
2
- import { S as StudioApiAdapter, P as PreviewDocumentStore, M as MediaProcessingJobState } from './mediaProxyPreview-DZD3iu_2.js';
3
- export { D as DEFAULT_HISTORY_ROOT, a as HISTORY_START, H as HistoryEntry, b as HistoryFileChange, c as HistoryListItem, d as HistoryResult, e as HistoryWho, f as HistoryWindow, L as LintResult, g as PreviewApiAdapter, h as ProjectHistory, i as ProjectHistoryOptions, R as RenderJobState, j as ResolvedProject, k as StudioSelectionResponse, l as StudioSelectionSnapshot, m as StudioSelectionTextField, o as openProjectHistory } from './mediaProxyPreview-DZD3iu_2.js';
2
+ import { S as StudioApiAdapter, P as PreviewDocumentStore, M as MediaProcessingJobState } from './mediaProxyPreview-CIayJ4Y4.js';
3
+ export { D as DEFAULT_HISTORY_ROOT, a as HISTORY_START, H as HistoryEntry, b as HistoryFileChange, c as HistoryListItem, d as HistoryResult, e as HistoryWho, f as HistoryWindow, L as LintResult, g as PreviewApiAdapter, h as ProjectHistory, i as ProjectHistoryOptions, R as RenderJobState, j as ResolvedProject, k as StudioSelectionResponse, l as StudioSelectionSnapshot, m as StudioSelectionTextField, o as openProjectHistory } from './mediaProxyPreview-CIayJ4Y4.js';
4
4
  import { SourceMutationTarget } from './helpers/sourceMutation.js';
5
5
  export { PatchOperation, dedupeClonedCompositionId, findTargetElement, parseSourceDocument, patchElementInHtml, removeElementFromHtml, splitElementInHtml } from './helpers/sourceMutation.js';
6
6
  export { ScreenshotClip, getElementScreenshotClip } from './helpers/screenshotClip.js';
@@ -52,6 +52,11 @@ declare function createProjectSignature(projectDir: string, excluding?: Readonly
52
52
  */
53
53
  declare function compositionsAffectedBy(projectDir: string, changedPath: string): string[] | null;
54
54
 
55
+ declare class HistoryBusyError extends Error {
56
+ readonly pid: number;
57
+ constructor(pid: number);
58
+ }
59
+
55
60
  /** Recursively walk a directory and return relative file paths. */
56
61
  declare function walkDir(dir: string, prefix?: string): string[];
57
62
 
@@ -199,4 +204,4 @@ type BackgroundRemovalRender = (options: {
199
204
  }>;
200
205
  declare function createBackgroundRemovalJob(opts: BackgroundRemovalJobOptions, render: BackgroundRemovalRender): MediaProcessingJobState;
201
206
 
202
- export { type AppliedFileMutation, type BackgroundRemovalRender, DELETED_VERSION, type DuplicateElementResult, type FileMutationInput, type FileWriteReceipt, MIME_TYPES, MediaProcessingJobState, PREVIEW_BUNDLE_OPTIONS, SourceMutationTarget, StudioApiAdapter, type ThumbnailOutputDimensions, affectsProjectSignature, applyFileMutations, buildSubCompositionHtml, compositionsAffectedBy, consumeFileWriteReceipt, createBackgroundRemovalJob, createPreviewDocumentStore, createProjectSignature, createStudioApi, duplicateElementInHtml, fileContentVersion, getMimeType, identifyFileWrite, requestSubPath, settledFileTag, stampProjectHfIds, thumbnailDeviceScaleFactor, walkDir };
207
+ export { type AppliedFileMutation, type BackgroundRemovalRender, DELETED_VERSION, type DuplicateElementResult, type FileMutationInput, type FileWriteReceipt, HistoryBusyError, MIME_TYPES, MediaProcessingJobState, PREVIEW_BUNDLE_OPTIONS, SourceMutationTarget, StudioApiAdapter, type ThumbnailOutputDimensions, affectsProjectSignature, applyFileMutations, buildSubCompositionHtml, compositionsAffectedBy, consumeFileWriteReceipt, createBackgroundRemovalJob, createPreviewDocumentStore, createProjectSignature, createStudioApi, duplicateElementInHtml, fileContentVersion, getMimeType, identifyFileWrite, requestSubPath, settledFileTag, stampProjectHfIds, thumbnailDeviceScaleFactor, walkDir };
package/dist/index.js CHANGED
@@ -22,7 +22,7 @@ import {
22
22
  isAutoProxyEnabled,
23
23
  proxyEtagSalt,
24
24
  resolvePreviewMediaCodecProbeCache
25
- } from "./chunk-5PGWTBCL.js";
25
+ } from "./chunk-X3T65KC2.js";
26
26
  import {
27
27
  ProxyCapacityError,
28
28
  ProxyTranscodeError,
@@ -1133,6 +1133,7 @@ function requestSubPath(url, route) {
1133
1133
  }
1134
1134
 
1135
1135
  // src/routes/files.ts
1136
+ import { insertBeforeCloseTag } from "@hyperframes/core/compiler/html-document";
1136
1137
  async function loadGsapParser() {
1137
1138
  return import("@hyperframes/parsers/gsap-parser-recast");
1138
1139
  }
@@ -1679,8 +1680,8 @@ async function prepareGsapMutationScript(c, res, firstMutation) {
1679
1680
  `window.__timelines["${compId}"] = tl;`,
1680
1681
  "</script>"
1681
1682
  ].join("\n");
1682
- html = html.includes("</body>") ? html.replace("</body>", `${bootstrap}
1683
- </body>`) : `${html}
1683
+ html = insertBeforeCloseTag(html, "body", `${bootstrap}
1684
+ `) ?? `${html}
1684
1685
  ${bootstrap}`;
1685
1686
  block = extractGsapScriptBlock(html);
1686
1687
  }
@@ -3325,6 +3326,7 @@ import { join as join9, resolve as resolve4 } from "path";
3325
3326
  import { createHash as createHash4 } from "crypto";
3326
3327
  import {
3327
3328
  injectScriptsIntoHtml,
3329
+ insertBeforeCloseTag as insertBeforeCloseTag2,
3328
3330
  stripEmbeddedRuntimeScripts as stripEmbeddedRuntimeScripts2
3329
3331
  } from "@hyperframes/core/compiler";
3330
3332
  import { isWithinProjectRoot } from "@hyperframes/parsers/asset-resolution";
@@ -3550,9 +3552,8 @@ function injectProjectSignature(html, signature) {
3550
3552
  tag
3551
3553
  );
3552
3554
  }
3553
- if (html.includes("</head>")) return html.replace("</head>", `${tag}
3554
- </head>`);
3555
- return `${tag}
3555
+ return insertBeforeCloseTag2(html, "head", `${tag}
3556
+ `) ?? `${tag}
3556
3557
  ${html}`;
3557
3558
  }
3558
3559
  function readStudioMotionManifestContent(projectDir) {
@@ -3577,9 +3578,8 @@ function parseStudioMotionManifestContent(content) {
3577
3578
  }
3578
3579
  }
3579
3580
  function injectScriptTagIntoHead(html, scriptTag) {
3580
- if (html.includes("</head>")) return html.replace("</head>", `${scriptTag}
3581
- </head>`);
3582
- return `${scriptTag}
3581
+ return insertBeforeCloseTag2(html, "head", `${scriptTag}
3582
+ `) ?? `${scriptTag}
3583
3583
  ${html}`;
3584
3584
  }
3585
3585
  function htmlHasGsap(html) {
@@ -3749,8 +3749,8 @@ function registerPreviewRoutes(api, adapter) {
3749
3749
  }
3750
3750
  if (!bundled.includes("hyperframe.runtime") && !bundled.includes("hyperframes-preview-runtime")) {
3751
3751
  const runtimeTag = `<script src="${adapter.runtimeUrl}"></script>`;
3752
- bundled = bundled.includes("</body>") ? bundled.replace("</body>", `${runtimeTag}
3753
- </body>`) : bundled + `
3752
+ bundled = insertBeforeCloseTag2(bundled, "body", `${runtimeTag}
3753
+ `) ?? `${bundled}
3754
3754
  ${runtimeTag}`;
3755
3755
  }
3756
3756
  const baseHref = `/api/projects/${project.id}/preview/`;
@@ -5320,11 +5320,11 @@ function createStudioApi(adapter) {
5320
5320
  }
5321
5321
 
5322
5322
  // src/history/projectHistory.ts
5323
- import { randomUUID as randomUUID7 } from "crypto";
5323
+ import { randomUUID as randomUUID8 } from "crypto";
5324
5324
  import { rm as rm2 } from "fs/promises";
5325
- import { existsSync as existsSync12, mkdirSync as mkdirSync10, readFileSync as readFileSync17 } from "fs";
5325
+ import { existsSync as existsSync12, mkdirSync as mkdirSync11, readFileSync as readFileSync18 } from "fs";
5326
5326
  import { homedir as homedir2 } from "os";
5327
- import { join as join18, relative as relative4, resolve as resolve7, sep as sep3 } from "path";
5327
+ import { join as join19, relative as relative4, resolve as resolve7, sep as sep3 } from "path";
5328
5328
 
5329
5329
  // src/history/blobStore.ts
5330
5330
  import { createHash as createHash6, randomUUID as randomUUID5 } from "crypto";
@@ -5428,8 +5428,89 @@ function projectHistoryId(projectDir, historyRoot) {
5428
5428
  return id;
5429
5429
  }
5430
5430
 
5431
+ // src/history/ownerLock.ts
5432
+ import { randomUUID as randomUUID7 } from "crypto";
5433
+ import { linkSync, mkdirSync as mkdirSync9, readFileSync as readFileSync16, rmSync as rmSync5, writeFileSync as writeFileSync8 } from "fs";
5434
+ import { join as join18 } from "path";
5435
+ var HistoryBusyError = class extends Error {
5436
+ constructor(pid) {
5437
+ super(`This project's history is open in another process (pid ${pid}).`);
5438
+ this.pid = pid;
5439
+ this.name = "HistoryBusyError";
5440
+ }
5441
+ pid;
5442
+ };
5443
+ function alive(pid) {
5444
+ if (!Number.isInteger(pid) || pid <= 0) return false;
5445
+ try {
5446
+ process.kill(pid, 0);
5447
+ return true;
5448
+ } catch (error) {
5449
+ return error.code === "EPERM";
5450
+ }
5451
+ }
5452
+ function ownerOf(file) {
5453
+ try {
5454
+ const text2 = readFileSync16(file, "utf-8");
5455
+ return /^\d+$/.test(text2) ? Number(text2) : Number.NaN;
5456
+ } catch (error) {
5457
+ if (error.code === "ENOENT") return null;
5458
+ throw error;
5459
+ }
5460
+ }
5461
+ function claim(file) {
5462
+ const draft = `${file}-${randomUUID7()}.tmp`;
5463
+ writeFileSync8(draft, String(process.pid));
5464
+ try {
5465
+ linkSync(draft, file);
5466
+ return true;
5467
+ } catch (error) {
5468
+ if (error.code !== "EEXIST") throw error;
5469
+ return false;
5470
+ } finally {
5471
+ rmSync5(draft, { force: true });
5472
+ }
5473
+ }
5474
+ function releaseOwn(file) {
5475
+ if (ownerOf(file) === process.pid) rmSync5(file, { force: true });
5476
+ }
5477
+ function evictDeadOwner(file) {
5478
+ const evictor = `${file}.evict`;
5479
+ if (!claim(evictor)) {
5480
+ const holder = ownerOf(evictor);
5481
+ if (holder !== null && !alive(holder)) rmSync5(evictor, { force: true });
5482
+ return false;
5483
+ }
5484
+ try {
5485
+ const pid = ownerOf(file);
5486
+ if (pid !== null && !alive(pid)) rmSync5(file, { force: true });
5487
+ return true;
5488
+ } finally {
5489
+ releaseOwn(evictor);
5490
+ }
5491
+ }
5492
+ async function takeHistoryOwnership(home, waitMs) {
5493
+ const file = join18(home, "owner.pid");
5494
+ const deadline = Date.now() + waitMs;
5495
+ mkdirSync9(home, { recursive: true });
5496
+ for (; ; ) {
5497
+ if (claim(file)) {
5498
+ let held = true;
5499
+ return () => {
5500
+ if (held) releaseOwn(file);
5501
+ held = false;
5502
+ };
5503
+ }
5504
+ const pid = ownerOf(file);
5505
+ if (pid === null) continue;
5506
+ if (!alive(pid) && evictDeadOwner(file)) continue;
5507
+ if (Date.now() >= deadline) throw new HistoryBusyError(pid);
5508
+ await new Promise((settle) => setTimeout(settle, 50));
5509
+ }
5510
+ }
5511
+
5431
5512
  // src/history/historyLog.ts
5432
- import { closeSync as closeSync5, constants as constants4, mkdirSync as mkdirSync9, openSync as openSync5, readFileSync as readFileSync16, writeSync as writeSync2 } from "fs";
5513
+ import { closeSync as closeSync5, constants as constants4, mkdirSync as mkdirSync10, openSync as openSync5, readFileSync as readFileSync17, writeSync as writeSync2 } from "fs";
5433
5514
  import { dirname as dirname7 } from "path";
5434
5515
  var START = "start";
5435
5516
  function parseRecord(line) {
@@ -5442,7 +5523,7 @@ function parseRecord(line) {
5442
5523
  function readLog(file, onUnreadable) {
5443
5524
  let text2;
5444
5525
  try {
5445
- text2 = readFileSync16(file, "utf-8");
5526
+ text2 = readFileSync17(file, "utf-8");
5446
5527
  } catch {
5447
5528
  return null;
5448
5529
  }
@@ -5482,7 +5563,7 @@ function writeLog(file, log) {
5482
5563
  ...log.entries.map((entry) => ({ type: "entry", entry })),
5483
5564
  ...[...log.pins].map((id) => ({ type: "pin", id, pinned: true }))
5484
5565
  ];
5485
- mkdirSync9(dirname7(file), { recursive: true });
5566
+ mkdirSync10(dirname7(file), { recursive: true });
5486
5567
  replaceFileAtomically(file, records.map((r) => `${JSON.stringify(r)}
5487
5568
  `).join(""), 420);
5488
5569
  }
@@ -5540,7 +5621,7 @@ function referencedHashes(log, current) {
5540
5621
  }
5541
5622
 
5542
5623
  // src/history/projectHistory.ts
5543
- var DEFAULT_HISTORY_ROOT = join18(homedir2(), ".cache", "hyperframes", "history");
5624
+ var DEFAULT_HISTORY_ROOT = join19(homedir2(), ".cache", "hyperframes", "history");
5544
5625
  var OUTSIDE = { kind: "outside", name: "Outside" };
5545
5626
  var OUTSIDE_LABEL = "Changed outside the app";
5546
5627
  var RACY_MS = 2e3;
@@ -5566,7 +5647,7 @@ var Engine = class {
5566
5647
  this.projectId = projectId;
5567
5648
  this.blobs = blobs;
5568
5649
  this.dir = resolve7(options.projectDir);
5569
- this.home = join18(options.historyRoot, projectId);
5650
+ this.home = join19(options.historyRoot, projectId);
5570
5651
  }
5571
5652
  options;
5572
5653
  projectId;
@@ -5586,7 +5667,7 @@ var Engine = class {
5586
5667
  listeners = /* @__PURE__ */ new Set();
5587
5668
  tail = Promise.resolve();
5588
5669
  get logFile() {
5589
- return join18(this.home, "log.jsonl");
5670
+ return join19(this.home, "log.jsonl");
5590
5671
  }
5591
5672
  now() {
5592
5673
  return (this.options.now ?? Date.now)();
@@ -5619,7 +5700,7 @@ var Engine = class {
5619
5700
  const sweptAt = Date.now();
5620
5701
  for (const file of listProjectFiles(this.dir))
5621
5702
  this.tracked.set(file.path, {
5622
- hash: await this.blobs.put(join18(this.dir, file.path)),
5703
+ hash: await this.blobs.put(join19(this.dir, file.path)),
5623
5704
  stat: statKey(file, sweptAt)
5624
5705
  });
5625
5706
  this.log.baseline = this.manifest();
@@ -5631,7 +5712,7 @@ var Engine = class {
5631
5712
  }
5632
5713
  readStatCache() {
5633
5714
  try {
5634
- const files = JSON.parse(readFileSync17(join18(this.home, "stat.json"), "utf-8"));
5715
+ const files = JSON.parse(readFileSync18(join19(this.home, "stat.json"), "utf-8"));
5635
5716
  return new Map(Object.entries(files));
5636
5717
  } catch {
5637
5718
  return /* @__PURE__ */ new Map();
@@ -5639,8 +5720,8 @@ var Engine = class {
5639
5720
  }
5640
5721
  saveStatCache() {
5641
5722
  const files = JSON.stringify(Object.fromEntries(this.tracked));
5642
- mkdirSync10(this.home, { recursive: true });
5643
- replaceFileAtomically(join18(this.home, "stat.json"), files, 420);
5723
+ mkdirSync11(this.home, { recursive: true });
5724
+ replaceFileAtomically(join19(this.home, "stat.json"), files, 420);
5644
5725
  }
5645
5726
  /**
5646
5727
  * Takes in every write since the last sweep and files each change to its writer. ponytail: always the whole
@@ -5665,7 +5746,7 @@ var Engine = class {
5665
5746
  /** The file's hash once copied in; null when it was removed before the copy (the next sweep records that). */
5666
5747
  async storeIfPresent(path) {
5667
5748
  try {
5668
- return await this.blobs.put(join18(this.dir, path));
5749
+ return await this.blobs.put(join19(this.dir, path));
5669
5750
  } catch (error) {
5670
5751
  if (error.code === "ENOENT") return null;
5671
5752
  throw error;
@@ -5790,7 +5871,7 @@ var Engine = class {
5790
5871
  return this.outside;
5791
5872
  }
5792
5873
  newGroup(who, label) {
5793
- return { id: randomUUID7(), who, label, startedAt: this.now(), changes: /* @__PURE__ */ new Map() };
5874
+ return { id: randomUUID8(), who, label, startedAt: this.now(), changes: /* @__PURE__ */ new Map() };
5794
5875
  }
5795
5876
  async commitOutside() {
5796
5877
  clearTimeout(this.quietTimer);
@@ -5904,7 +5985,7 @@ var Engine = class {
5904
5985
  }
5905
5986
  /** Writes one project file (null deletes it), first leaving the running operation's receipt for its echo. */
5906
5987
  async writeProjectFile(path, hash) {
5907
- const absPath = join18(this.dir, path);
5988
+ const absPath = join19(this.dir, path);
5908
5989
  const { writeToken } = this;
5909
5990
  if (writeToken) {
5910
5991
  const version = hash === null ? DELETED_VERSION : hashVersion(hash);
@@ -6033,10 +6114,23 @@ var Engine = class {
6033
6114
  };
6034
6115
  async function openProjectHistory(options) {
6035
6116
  const projectId = projectHistoryId(options.projectDir, options.historyRoot);
6036
- const blobs = await openBlobStore(join18(options.historyRoot, projectId, "blobs"));
6037
- const engine = new Engine(options, projectId, blobs);
6038
- await engine.queue(() => engine.start());
6039
- return engine.api();
6117
+ const release = await takeHistoryOwnership(
6118
+ join19(options.historyRoot, projectId),
6119
+ options.ownerWaitMs ?? 5e3
6120
+ );
6121
+ try {
6122
+ const blobs = await openBlobStore(join19(options.historyRoot, projectId, "blobs"));
6123
+ const engine = new Engine(options, projectId, blobs);
6124
+ await engine.queue(() => engine.start());
6125
+ const api = engine.api();
6126
+ return {
6127
+ ...api,
6128
+ close: () => api.close().finally(release)
6129
+ };
6130
+ } catch (error) {
6131
+ release();
6132
+ throw error;
6133
+ }
6040
6134
  }
6041
6135
 
6042
6136
  // src/helpers/duplicateElement.ts
@@ -6092,26 +6186,26 @@ function numericAttribute(element, name) {
6092
6186
 
6093
6187
  // src/helpers/previewDocumentStore.ts
6094
6188
  import { createHash as createHash7 } from "crypto";
6095
- import { mkdirSync as mkdirSync11, readFileSync as readFileSync18, readdirSync as readdirSync7, rmSync as rmSync5 } from "fs";
6096
- import { join as join19 } from "path";
6189
+ import { mkdirSync as mkdirSync12, readFileSync as readFileSync19, readdirSync as readdirSync7, rmSync as rmSync6 } from "fs";
6190
+ import { join as join20 } from "path";
6097
6191
  function createPreviewDocumentStore(dir, salt) {
6098
- const fileFor = (key) => join19(dir, `${createHash7("sha256").update(`${salt}
6192
+ const fileFor = (key) => join20(dir, `${createHash7("sha256").update(`${salt}
6099
6193
  ${key}`).digest("hex").slice(0, 32)}.html`);
6100
6194
  return {
6101
6195
  read(key) {
6102
6196
  try {
6103
- return readFileSync18(fileFor(key), "utf-8");
6197
+ return readFileSync19(fileFor(key), "utf-8");
6104
6198
  } catch {
6105
6199
  return null;
6106
6200
  }
6107
6201
  },
6108
6202
  write(key, html) {
6109
6203
  try {
6110
- mkdirSync11(dir, { recursive: true });
6204
+ mkdirSync12(dir, { recursive: true });
6111
6205
  const file = fileFor(key);
6112
6206
  replaceFileAtomically(file, html, 420);
6113
6207
  for (const name of readdirSync7(dir)) {
6114
- if (join19(dir, name) !== file) rmSync5(join19(dir, name), { force: true });
6208
+ if (join20(dir, name) !== file) rmSync6(join20(dir, name), { force: true });
6115
6209
  }
6116
6210
  } catch {
6117
6211
  }
@@ -6190,6 +6284,7 @@ export {
6190
6284
  DEFAULT_HISTORY_ROOT,
6191
6285
  DELETED_VERSION,
6192
6286
  START as HISTORY_START,
6287
+ HistoryBusyError,
6193
6288
  MIME_TYPES,
6194
6289
  PREVIEW_BUNDLE_OPTIONS,
6195
6290
  STUDIO_MANUAL_EDITS_PATH,