@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.
- package/dist/{chunk-5PGWTBCL.js → chunk-X3T65KC2.js} +5 -8
- package/dist/chunk-X3T65KC2.js.map +1 -0
- package/dist/helpers/mediaProxyPreview.d.ts +1 -1
- package/dist/helpers/mediaProxyPreview.js +1 -1
- package/dist/index.d.ts +8 -3
- package/dist/index.js +132 -37
- package/dist/index.js.map +1 -1
- package/dist/{mediaProxyPreview-DZD3iu_2.d.ts → mediaProxyPreview-CIayJ4Y4.d.ts} +2 -0
- package/package.json +3 -3
- package/dist/chunk-5PGWTBCL.js.map +0 -1
|
@@ -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
|
|
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-
|
|
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-
|
|
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';
|
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-
|
|
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-
|
|
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-
|
|
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
|
|
1683
|
-
|
|
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
|
-
|
|
3554
|
-
|
|
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
|
-
|
|
3581
|
-
|
|
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
|
|
3753
|
-
|
|
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
|
|
5323
|
+
import { randomUUID as randomUUID8 } from "crypto";
|
|
5324
5324
|
import { rm as rm2 } from "fs/promises";
|
|
5325
|
-
import { existsSync as existsSync12, mkdirSync as
|
|
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
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
|
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(
|
|
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(
|
|
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
|
-
|
|
5643
|
-
replaceFileAtomically(
|
|
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(
|
|
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:
|
|
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 =
|
|
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
|
|
6037
|
-
|
|
6038
|
-
|
|
6039
|
-
|
|
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
|
|
6096
|
-
import { join as
|
|
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) =>
|
|
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
|
|
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
|
-
|
|
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 (
|
|
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,
|