@hyperframes/studio-server 0.8.83 → 0.8.84

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.
Files changed (36) hide show
  1. package/dist/{chunk-EL3UQI46.js → chunk-2YEGCDJW.js} +21 -8
  2. package/dist/chunk-2YEGCDJW.js.map +1 -0
  3. package/dist/{chunk-WQ2WF2YJ.js → chunk-I3DRHG52.js} +7 -81
  4. package/dist/chunk-I3DRHG52.js.map +1 -0
  5. package/dist/chunk-IG4DRZI2.js +1571 -0
  6. package/dist/chunk-IG4DRZI2.js.map +1 -0
  7. package/dist/chunk-PMXUKOC3.js +22 -0
  8. package/dist/chunk-PMXUKOC3.js.map +1 -0
  9. package/dist/chunk-TTWLTYSQ.js +79 -0
  10. package/dist/chunk-TTWLTYSQ.js.map +1 -0
  11. package/dist/chunk-WUHW3MWG.js +100 -0
  12. package/dist/chunk-WUHW3MWG.js.map +1 -0
  13. package/dist/{chunk-HWBORC2U.js → chunk-YUPEBATT.js} +3 -3
  14. package/dist/{chunk-HWBORC2U.js.map → chunk-YUPEBATT.js.map} +1 -1
  15. package/dist/helpers/hfIdPersist.d.ts +23 -0
  16. package/dist/helpers/hfIdPersist.js +13 -0
  17. package/dist/helpers/hfIdPersist.js.map +1 -0
  18. package/dist/helpers/mediaCodecMap.js +2 -1
  19. package/dist/helpers/mediaProxyPreview.d.ts +2 -1
  20. package/dist/helpers/mediaProxyPreview.js +4 -3
  21. package/dist/helpers/proxyTranscoder.d.ts +4 -1
  22. package/dist/helpers/proxyTranscoder.js +7 -2
  23. package/dist/helpers/safePath.d.ts +22 -0
  24. package/dist/helpers/safePath.js +27 -0
  25. package/dist/helpers/safePath.js.map +1 -0
  26. package/dist/history/index.d.ts +49 -0
  27. package/dist/history/index.js +29 -0
  28. package/dist/history/index.js.map +1 -0
  29. package/dist/index.d.ts +9 -58
  30. package/dist/index.js +302 -1897
  31. package/dist/index.js.map +1 -1
  32. package/dist/{mediaProxyPreview-BMHd9G3N.d.ts → mediaProxyPreview-CLu9FdIr.d.ts} +2 -139
  33. package/dist/projectHistory-Cm8Mdmmv.d.ts +139 -0
  34. package/package.json +15 -3
  35. package/dist/chunk-EL3UQI46.js.map +0 -1
  36. package/dist/chunk-WQ2WF2YJ.js.map +0 -1
package/dist/index.js CHANGED
@@ -14,6 +14,39 @@ import {
14
14
  unwrapElementsFromHtml,
15
15
  wrapElementsInHtml
16
16
  } from "./chunk-W6TFBTP2.js";
17
+ import {
18
+ isCompositionSource,
19
+ stampProjectHfIds
20
+ } from "./chunk-WUHW3MWG.js";
21
+ import {
22
+ DEFAULT_HISTORY_ROOT,
23
+ DELETED_VERSION,
24
+ HistoryBusyError,
25
+ HistoryClosedError,
26
+ HistoryIdError,
27
+ MAX_WINDOW_IDLE_MS,
28
+ START,
29
+ STUDIO_SIGNATURE_MANIFEST_PATHS,
30
+ UNDO_MODES,
31
+ affectsProjectSignature,
32
+ clearFileWriteReceipt,
33
+ consumeFileWriteReceipt,
34
+ createProjectSignature,
35
+ createWriteToken,
36
+ fileContentVersion,
37
+ historyCache,
38
+ identifyFileWrite,
39
+ listProjectHistories,
40
+ openProjectHistory,
41
+ pruneGoneProjectHistories,
42
+ recordFileWriteReceipt,
43
+ resolveProjectAndSignature,
44
+ resolveProjectSignature,
45
+ settledFileTag
46
+ } from "./chunk-IG4DRZI2.js";
47
+ import {
48
+ replaceFileAtomically
49
+ } from "./chunk-PMXUKOC3.js";
17
50
  import {
18
51
  getElementScreenshotClip
19
52
  } from "./chunk-W2SBTCO2.js";
@@ -22,31 +55,37 @@ import {
22
55
  isAutoProxyEnabled,
23
56
  proxyEtagSalt,
24
57
  resolvePreviewMediaCodecProbeCache
25
- } from "./chunk-HWBORC2U.js";
58
+ } from "./chunk-YUPEBATT.js";
26
59
  import {
60
+ PROXY_PENDING_RETRY_AFTER_SECONDS,
27
61
  ProxyCapacityError,
28
62
  ProxyTranscodeError,
29
- resolveProxy
30
- } from "./chunk-EL3UQI46.js";
63
+ ProxyWaitTimeoutError,
64
+ proxyActivityMark,
65
+ resolveProxy,
66
+ waitForProxy
67
+ } from "./chunk-2YEGCDJW.js";
31
68
  import {
32
69
  PROXY_VARIANT_CONFIG,
33
70
  decideMediaProxyEligibility,
71
+ isProxyVariantRequest,
72
+ probeAssetCodec,
73
+ probeMediaMetadata,
74
+ recordProxyRequest,
75
+ resolveProxyVariantRequest
76
+ } from "./chunk-I3DRHG52.js";
77
+ import {
34
78
  folderGone,
35
79
  isInHiddenOrVendorDir,
36
80
  isProjectRootMissing,
37
- isProxyVariantRequest,
38
81
  isSafePath,
39
82
  mkdirWithinProject,
40
83
  pinWithinProject,
41
- probeAssetCodec,
42
- probeMediaMetadata,
43
84
  realFilePath,
44
85
  realpath,
45
- recordProxyRequest,
46
- resolveProxyVariantRequest,
47
86
  resolveWithinProject,
48
87
  walkDir
49
- } from "./chunk-WQ2WF2YJ.js";
88
+ } from "./chunk-TTWLTYSQ.js";
50
89
  import {
51
90
  STUDIO_MANUAL_EDITS_PATH,
52
91
  createStudioManualEditsRenderBodyScript,
@@ -62,300 +101,13 @@ import { Hono } from "hono";
62
101
 
63
102
  // src/routes/projects.ts
64
103
  import { readFile } from "fs/promises";
65
- import { join as join2 } from "path";
66
-
67
- // src/helpers/hfIdPersist.ts
68
- import { ensureHfIds } from "@hyperframes/parsers/hf-ids";
69
- import { createHash } from "crypto";
70
- import { closeSync, constants, fstatSync, openSync, readFileSync, writeFileSync } from "fs";
71
- import { dirname, join } from "path";
72
-
73
- // src/helpers/atomicFile.ts
74
- import * as fs from "fs";
75
- import { randomUUID } from "crypto";
76
- function replaceFileAtomically(filePath, content, mode, operations = fs) {
77
- const tempPath = `${filePath}.${process.pid}.${randomUUID()}.tmp`;
78
- try {
79
- operations.writeFileSync(tempPath, content, { encoding: "utf-8", mode });
80
- operations.chmodSync(tempPath, mode);
81
- operations.renameSync(tempPath, filePath);
82
- } catch (error) {
83
- try {
84
- operations.unlinkSync(tempPath);
85
- } catch {
86
- }
87
- throw error;
88
- }
89
- }
90
-
91
- // src/helpers/hfIdPersist.ts
92
- var isCompositionSource = (html) => /data-composition-id\s*=/.test(html);
93
- var STAMP_RECORD = join(".hyperframes", "hf-ids-stamped.json");
94
- var contentHash = (text2) => createHash("sha1").update(text2).digest("base64url");
95
- function stampProjectHfIds(projectDir) {
96
- const recordPath = join(projectDir, STAMP_RECORD);
97
- let last = {};
98
- try {
99
- last = JSON.parse(readFileSync(recordPath, "utf-8"));
100
- } catch {
101
- }
102
- const next = {};
103
- for (const file of walkDir(projectDir)) {
104
- if (!file.endsWith(".html") || isInHiddenOrVendorDir(file)) continue;
105
- const absPath = join(projectDir, file);
106
- let html;
107
- try {
108
- html = readFileSync(absPath, "utf-8");
109
- } catch {
110
- continue;
111
- }
112
- let hash = contentHash(html);
113
- if (last[file] !== hash && isCompositionSource(html)) {
114
- const stamped = stampFileHfIds(absPath);
115
- if (stamped === null) continue;
116
- hash = contentHash(stamped);
117
- }
118
- next[file] = hash;
119
- }
120
- try {
121
- mkdirWithinProject(projectDir, dirname(recordPath));
122
- writeFileSync(recordPath, JSON.stringify(next));
123
- } catch {
124
- }
125
- }
126
- function openNoFollow(filePath, flags) {
127
- const noFollow = constants.O_NOFOLLOW ?? 0;
128
- try {
129
- return openSync(filePath, flags | noFollow);
130
- } catch {
131
- return null;
132
- }
133
- }
134
- function isUnchanged(filePath, expected) {
135
- const fd = openNoFollow(filePath, constants.O_RDONLY);
136
- if (fd === null) return false;
137
- try {
138
- return readFileSync(fd, "utf-8") === expected;
139
- } finally {
140
- closeSync(fd);
141
- }
142
- }
143
- function stampFileHfIds(filePath) {
144
- let fd = openNoFollow(filePath, constants.O_RDWR);
145
- let writable = true;
146
- if (fd === null) {
147
- fd = openNoFollow(filePath, constants.O_RDONLY);
148
- writable = false;
149
- }
150
- if (fd === null) return null;
151
- try {
152
- if (!fstatSync(fd).isFile()) return null;
153
- const html = readFileSync(fd, "utf-8");
154
- const normalized = ensureHfIds(html);
155
- const idsBefore = (html.match(/\bdata-hf-id=/g) ?? []).length;
156
- const idsAfter = (normalized.match(/\bdata-hf-id=/g) ?? []).length;
157
- if (writable && idsAfter > idsBefore) {
158
- const mode = fstatSync(fd).mode;
159
- closeSync(fd);
160
- fd = null;
161
- if (isUnchanged(filePath, html)) replaceFileAtomically(filePath, normalized, mode);
162
- }
163
- return normalized;
164
- } catch (err) {
165
- console.warn("[hyperframes] stampFileHfIds: failed to stamp ids:", err);
166
- return null;
167
- } finally {
168
- if (fd !== null) closeSync(fd);
169
- }
170
- }
171
-
172
- // src/helpers/projectSignature.ts
173
- import { createHash as createHash2 } from "crypto";
174
- import { lstatSync, readFileSync as readFileSync2, readdirSync } from "fs";
175
- import { extname, isAbsolute, relative, resolve, sep } from "path";
176
- var SIGNATURE_TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
177
- ".cjs",
178
- ".css",
179
- ".html",
180
- ".js",
181
- ".json",
182
- ".jsx",
183
- ".mjs",
184
- ".svg",
185
- ".ts",
186
- ".tsx"
187
- ]);
188
- var SIGNATURE_EXCLUDED_DIRS = /* @__PURE__ */ new Set([
189
- ".cache",
190
- ".git",
191
- ".hyperframes",
192
- ".next",
193
- ".thumbnails",
194
- ".transcode-cache",
195
- ".vite",
196
- ".waveform-cache",
197
- "build",
198
- "coverage",
199
- "dist",
200
- "node_modules",
201
- "outputs",
202
- "renders"
203
- ]);
204
- var MAX_SIGNATURE_TEXT_BYTES = 2e6;
205
- var STUDIO_SIGNATURE_MANIFEST_PATHS = [
206
- ".hyperframes/studio-manual-edits.json",
207
- ".hyperframes/studio-motion.json"
208
- ];
209
- function affectsProjectSignature(projectDir, changedPath) {
210
- const relativePath = relative(resolve(projectDir), resolve(changedPath));
211
- if (relativePath === "" || relativePath.startsWith("..") || isAbsolute(relativePath)) {
212
- return false;
213
- }
214
- const segments = relativePath.split(sep);
215
- if (STUDIO_SIGNATURE_MANIFEST_PATHS.includes(segments.join("/"))) return true;
216
- return !segments.some((segment) => SIGNATURE_EXCLUDED_DIRS.has(segment));
217
- }
218
- var projectSignatureCache = /* @__PURE__ */ new Map();
219
- function isPathWithin(parentDir, childPath) {
220
- const childRelativePath = relative(parentDir, childPath);
221
- return childRelativePath === "" || !childRelativePath.startsWith("..") && !isAbsolute(childRelativePath);
222
- }
223
- function isTextContentEligible(file, size) {
224
- return SIGNATURE_TEXT_EXTENSIONS.has(extname(file).toLowerCase()) && size <= MAX_SIGNATURE_TEXT_BYTES;
225
- }
226
- function collectProjectSignatureFiles(projectDir, dir, files) {
227
- let entries;
228
- try {
229
- entries = readdirSync(dir).sort();
230
- } catch {
231
- return;
232
- }
233
- for (const entry of entries) {
234
- if (SIGNATURE_EXCLUDED_DIRS.has(entry)) continue;
235
- const file = resolve(dir, entry);
236
- if (!isPathWithin(projectDir, file)) continue;
237
- let stat3;
238
- try {
239
- stat3 = lstatSync(file);
240
- } catch {
241
- continue;
242
- }
243
- if (stat3.isSymbolicLink()) continue;
244
- if (stat3.isDirectory()) {
245
- collectProjectSignatureFiles(projectDir, file, files);
246
- } else if (stat3.isFile()) {
247
- files.push({
248
- file,
249
- mtimeMs: stat3.mtimeMs,
250
- ctimeMs: stat3.ctimeMs,
251
- size: stat3.size,
252
- textContentEligible: isTextContentEligible(file, stat3.size)
253
- });
254
- }
255
- }
256
- }
257
- function collectProjectSignatureManifestFiles(projectDir, files) {
258
- const seen = new Set(files.map((entry) => entry.file));
259
- for (const manifestPath of STUDIO_SIGNATURE_MANIFEST_PATHS) {
260
- const file = resolve(projectDir, manifestPath);
261
- if (seen.has(file) || !isPathWithin(projectDir, file)) continue;
262
- let stat3;
263
- try {
264
- stat3 = lstatSync(file);
265
- } catch {
266
- continue;
267
- }
268
- if (stat3.isSymbolicLink() || !stat3.isFile()) continue;
269
- files.push({
270
- file,
271
- mtimeMs: stat3.mtimeMs,
272
- ctimeMs: stat3.ctimeMs,
273
- size: stat3.size,
274
- textContentEligible: isTextContentEligible(file, stat3.size)
275
- });
276
- seen.add(file);
277
- }
278
- }
279
- function createProjectFingerprint(projectDir, files) {
280
- const hash = createHash2("sha256");
281
- for (const entry of files) {
282
- hash.update(relative(projectDir, entry.file));
283
- hash.update("\0");
284
- hash.update(String(entry.size));
285
- hash.update("\0");
286
- hash.update(String(entry.mtimeMs));
287
- hash.update("\0");
288
- hash.update(String(entry.ctimeMs));
289
- hash.update("\0");
290
- hash.update(entry.textContentEligible ? "text" : "binary");
291
- hash.update("\0");
292
- }
293
- return hash.digest("hex").slice(0, 24);
294
- }
295
- function collectProjectFiles(normalizedProjectDir) {
296
- const collected = [];
297
- collectProjectSignatureFiles(normalizedProjectDir, normalizedProjectDir, collected);
298
- collectProjectSignatureManifestFiles(normalizedProjectDir, collected);
299
- return collected;
300
- }
301
- function listProjectFiles(projectDir) {
302
- const normalizedProjectDir = resolve(projectDir);
303
- return collectProjectFiles(normalizedProjectDir).map((entry) => ({
304
- path: relative(normalizedProjectDir, entry.file).split(sep).join("/"),
305
- size: entry.size,
306
- mtimeMs: entry.mtimeMs,
307
- ctimeMs: entry.ctimeMs
308
- }));
309
- }
310
- function resolveProjectSignature(adapter, projectDir) {
311
- return adapter.getProjectSignature?.(projectDir) ?? createProjectSignature(projectDir);
312
- }
313
- async function resolveProjectAndSignature(adapter, projectId) {
314
- const project = await adapter.resolveProject(projectId);
315
- if (!project) return null;
316
- return { project, signature: resolveProjectSignature(adapter, project.dir) };
317
- }
318
- function createProjectSignature(projectDir, excluding = /* @__PURE__ */ new Set()) {
319
- const normalizedProjectDir = resolve(projectDir);
320
- const collected = collectProjectFiles(normalizedProjectDir);
321
- const files = collected.filter(
322
- (entry) => !excluding.has(relative(normalizedProjectDir, entry.file).split(sep).join("/"))
323
- );
324
- files.sort((a, b) => a.file.localeCompare(b.file));
325
- const fingerprint = createProjectFingerprint(normalizedProjectDir, files);
326
- const cacheKey = excluding.size ? `${normalizedProjectDir}\0${createHash2("sha256").update([...excluding].sort().join("\0")).digest("hex")}` : normalizedProjectDir;
327
- const cached = projectSignatureCache.get(cacheKey);
328
- if (cached?.fingerprint === fingerprint) return cached.signature;
329
- const hash = createHash2("sha256");
330
- for (const entry of files) {
331
- const relativePath = relative(normalizedProjectDir, entry.file);
332
- hash.update(relativePath);
333
- hash.update("\0");
334
- hash.update(String(entry.size));
335
- hash.update("\0");
336
- if (entry.textContentEligible) {
337
- try {
338
- hash.update(readFileSync2(entry.file));
339
- } catch {
340
- hash.update(String(entry.mtimeMs));
341
- }
342
- } else {
343
- hash.update(String(entry.mtimeMs));
344
- }
345
- hash.update("\0");
346
- }
347
- const signature = hash.digest("hex").slice(0, 24);
348
- projectSignatureCache.set(cacheKey, { fingerprint, signature });
349
- return signature;
350
- }
351
-
352
- // src/routes/projects.ts
104
+ import { join } from "path";
353
105
  async function filterCompositionFiles(projectDir, files) {
354
106
  const htmlFiles = files.filter((f) => f.endsWith(".html") && !isInHiddenOrVendorDir(f));
355
107
  const checks = await Promise.all(
356
108
  htmlFiles.map(async (f) => {
357
109
  try {
358
- const content = await readFile(join2(projectDir, f), "utf-8");
110
+ const content = await readFile(join(projectDir, f), "utf-8");
359
111
  return isCompositionSource(content);
360
112
  } catch {
361
113
  return false;
@@ -395,22 +147,22 @@ function registerProjectRoutes(api, adapter) {
395
147
  // src/routes/files.ts
396
148
  import { bodyLimit } from "hono/body-limit";
397
149
  import {
398
- closeSync as closeSync2,
150
+ closeSync,
399
151
  existsSync as existsSync3,
400
- lstatSync as lstatSync3,
401
- openSync as openSync2,
402
- readFileSync as readFileSync6,
152
+ lstatSync as lstatSync2,
153
+ openSync,
154
+ readFileSync as readFileSync4,
403
155
  readlinkSync,
404
- writeFileSync as writeFileSync5,
156
+ writeFileSync as writeFileSync4,
405
157
  writeSync,
406
158
  unlinkSync as unlinkSync2,
407
159
  rmSync as rmSync3,
408
160
  statSync as statSync3,
409
- fstatSync as fstatSync2,
161
+ fstatSync,
410
162
  renameSync as renameSync2,
411
- readdirSync as readdirSync3
163
+ readdirSync as readdirSync2
412
164
  } from "fs";
413
- import { resolve as resolve3, dirname as dirname4, join as join6 } from "path";
165
+ import { resolve as resolve2, dirname as dirname3, join as join5 } from "path";
414
166
 
415
167
  // src/helpers/mime.ts
416
168
  var MIME_TYPES = {
@@ -462,15 +214,15 @@ function isAudioFile(name) {
462
214
  import { spawn } from "child_process";
463
215
  import {
464
216
  existsSync,
465
- writeFileSync as writeFileSync2,
217
+ writeFileSync,
466
218
  mkdirSync,
467
- lstatSync as lstatSync2,
219
+ lstatSync,
468
220
  mkdtempSync,
469
221
  renameSync,
470
222
  rmSync,
471
223
  statSync
472
224
  } from "fs";
473
- import { dirname as dirname2, join as join3 } from "path";
225
+ import { dirname, join as join2 } from "path";
474
226
  import { findFfBinary } from "@hyperframes/parsers/ff-binaries";
475
227
  var SAMPLE_RATE = 4e3;
476
228
  var PEAK_COUNT = 4e3;
@@ -537,20 +289,20 @@ function decodeAudioPeaks(audioPath) {
537
289
  });
538
290
  }
539
291
  async function generateWaveformCache(projectDir, assetPath) {
540
- const audioPath = join3(projectDir, assetPath);
292
+ const audioPath = join2(projectDir, assetPath);
541
293
  if (!existsSync(audioPath)) return;
542
294
  const stats = statSync(audioPath);
543
- const cacheDir = join3(projectDir, ".waveform-cache");
544
- const cachePath = join3(cacheDir, buildWaveformCacheKey(assetPath, stats));
295
+ const cacheDir = join2(projectDir, ".waveform-cache");
296
+ const cachePath = join2(cacheDir, buildWaveformCacheKey(assetPath, stats));
545
297
  if (isWaveformCacheDirectory(cacheDir) && existsSync(cachePath)) return;
546
298
  const peaks = await decodeAudioPeaks(audioPath);
547
299
  writeWaveformCache(cachePath, peaks);
548
300
  }
549
301
  function isWaveformCacheDirectory(cacheDir) {
550
- return lstatSync2(cacheDir, { throwIfNoEntry: false })?.isDirectory() ?? false;
302
+ return lstatSync(cacheDir, { throwIfNoEntry: false })?.isDirectory() ?? false;
551
303
  }
552
304
  function writeWaveformCache(cachePath, peaks) {
553
- const cacheDir = dirname2(cachePath);
305
+ const cacheDir = dirname(cachePath);
554
306
  try {
555
307
  mkdirSync(cacheDir, { mode: 448 });
556
308
  } catch (error) {
@@ -559,10 +311,10 @@ function writeWaveformCache(cachePath, peaks) {
559
311
  if (!isWaveformCacheDirectory(cacheDir)) {
560
312
  throw new Error("Waveform cache must be a directory, not a symlink");
561
313
  }
562
- const stagingDir = mkdtempSync(join3(cacheDir, ".waveform-"));
314
+ const stagingDir = mkdtempSync(join2(cacheDir, ".waveform-"));
563
315
  try {
564
- const stagingPath = join3(stagingDir, "peaks.json");
565
- writeFileSync2(stagingPath, JSON.stringify(peaks), { flag: "wx" });
316
+ const stagingPath = join2(stagingDir, "peaks.json");
317
+ writeFileSync(stagingPath, JSON.stringify(peaks), { flag: "wx" });
566
318
  renameSync(stagingPath, cachePath);
567
319
  } finally {
568
320
  try {
@@ -574,9 +326,9 @@ function writeWaveformCache(cachePath, peaks) {
574
326
 
575
327
  // src/helpers/mediaValidation.ts
576
328
  import { spawnSync } from "child_process";
577
- import { mkdtempSync as mkdtempSync2, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
329
+ import { mkdtempSync as mkdtempSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
578
330
  import { tmpdir } from "os";
579
- import { basename, join as join4 } from "path";
331
+ import { basename, join as join3 } from "path";
580
332
  import { FFPROBE_PATH_ENV, findFfBinary as findFfBinary2 } from "@hyperframes/parsers/ff-binaries";
581
333
  var VIDEO_EXT = /\.(mp4|webm|mov|mkv|avi|m4v|mxf|mts|m2ts|ts)$/i;
582
334
  var AUDIO_EXT = /\.(mp3|wav|ogg|m4a|aac|flac)$/i;
@@ -627,10 +379,10 @@ function checkStreams(stdout, isVideo, isAudio) {
627
379
  }
628
380
  }
629
381
  function validateUploadedMediaBuffer(fileName, buffer, runner = defaultRunner) {
630
- const tempDir = mkdtempSync2(join4(tmpdir(), "hyperframes-upload-"));
631
- const tempPath = join4(tempDir, basename(fileName));
382
+ const tempDir = mkdtempSync2(join3(tmpdir(), "hyperframes-upload-"));
383
+ const tempPath = join3(tempDir, basename(fileName));
632
384
  try {
633
- writeFileSync3(tempPath, buffer);
385
+ writeFileSync2(tempPath, buffer);
634
386
  return validateUploadedMedia(tempPath, runner);
635
387
  } finally {
636
388
  rmSync2(tempDir, { recursive: true, force: true });
@@ -638,9 +390,9 @@ function validateUploadedMediaBuffer(fileName, buffer, runner = defaultRunner) {
638
390
  }
639
391
 
640
392
  // src/helpers/backupJournal.ts
641
- import { readdirSync as readdirSync2, readFileSync as readFileSync3, unlinkSync, writeFileSync as writeFileSync4 } from "fs";
393
+ import { readdirSync, readFileSync, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
642
394
  import { Buffer as Buffer2 } from "buffer";
643
- import { join as join5, relative as relative2 } from "path";
395
+ import { join as join4, relative } from "path";
644
396
  var DEFAULT_KEEP_PER_FILE = 10;
645
397
  function backupKeyForPath(path) {
646
398
  return Buffer2.from(path, "utf-8").toString("base64url");
@@ -650,20 +402,20 @@ function timestampPrefix() {
650
402
  }
651
403
  function backupPathForResponse(projectDir, backupPath) {
652
404
  if (!backupPath) return null;
653
- const rel = relative2(projectDir, backupPath);
405
+ const rel = relative(projectDir, backupPath);
654
406
  if (!rel || rel.startsWith("..")) return null;
655
407
  return rel.split("\\").join("/");
656
408
  }
657
409
  function snapshotBeforeWrite(projectDir, absPath, options = {}) {
658
410
  if (!isSafePath(projectDir, absPath)) return { backupPath: null };
659
411
  try {
660
- const content = readFileSync3(absPath);
661
- const relativePath = relative2(projectDir, absPath);
662
- const backupDir = join5(projectDir, ".hyperframes", "backup");
412
+ const content = readFileSync(absPath);
413
+ const relativePath = relative(projectDir, absPath);
414
+ const backupDir = join4(projectDir, ".hyperframes", "backup");
663
415
  mkdirWithinProject(projectDir, backupDir);
664
416
  const backupKey = backupKeyForPath(relativePath);
665
417
  const backupPath = nextBackupPath(backupDir, backupKey);
666
- writeFileSync4(backupPath, content);
418
+ writeFileSync3(backupPath, content);
667
419
  pruneBackups(backupDir, backupKey, options.keepPerFile ?? DEFAULT_KEEP_PER_FILE);
668
420
  return { backupPath };
669
421
  } catch (error) {
@@ -676,18 +428,18 @@ function snapshotBeforeWrite(projectDir, absPath, options = {}) {
676
428
  }
677
429
  function nextBackupPath(backupDir, backupKey) {
678
430
  const base = `${timestampPrefix()}-${backupKey}`;
679
- let candidate = join5(backupDir, base);
431
+ let candidate = join4(backupDir, base);
680
432
  let counter = 2;
681
433
  while (true) {
682
434
  try {
683
- readFileSync3(candidate);
435
+ readFileSync(candidate);
684
436
  } catch (error) {
685
437
  if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
686
438
  return candidate;
687
439
  }
688
440
  throw error;
689
441
  }
690
- candidate = join5(backupDir, `${base}-${counter}`);
442
+ candidate = join4(backupDir, `${base}-${counter}`);
691
443
  counter += 1;
692
444
  }
693
445
  }
@@ -695,7 +447,7 @@ function pruneBackups(backupDir, backupKey, keepPerFile) {
695
447
  const keep = Math.max(1, Math.floor(keepPerFile));
696
448
  const suffix = `-${backupKey}`;
697
449
  const numberedSuffix = new RegExp(`-${backupKey}-\\d+$`);
698
- const matches = readdirSync2(backupDir).filter((name) => name.endsWith(suffix) || numberedSuffix.test(name)).map((name) => join5(backupDir, name)).sort((a, b) => {
450
+ const matches = readdirSync(backupDir).filter((name) => name.endsWith(suffix) || numberedSuffix.test(name)).map((name) => join4(backupDir, name)).sort((a, b) => {
699
451
  return b.localeCompare(a);
700
452
  });
701
453
  for (const file of matches.slice(keep)) {
@@ -715,96 +467,28 @@ function replaceWithProjectDirMissing(c, hostHeaders) {
715
467
  c.res = Response.json(PROJECT_DIR_MISSING, { status: 404, headers: hostHeaders });
716
468
  }
717
469
 
718
- // src/helpers/fileVersion.ts
719
- import { createHash as createHash3, randomUUID as randomUUID2 } from "crypto";
720
- var RECEIPT_TTL_MS = 1e4;
721
- var receipts = /* @__PURE__ */ new Map();
722
- function fileContentVersion(content) {
723
- return hashVersion(createHash3("sha256").update(content).digest("hex"));
724
- }
725
- var DELETED_VERSION = '"deleted"';
726
- function hashVersion(hex) {
727
- return `"sha256:${hex}"`;
728
- }
729
- function hashOfVersion(version) {
730
- return /^"sha256:([0-9a-f]{64})"$/.exec(version)?.[1];
731
- }
732
- function settledFileTag(stat3, now = Date.now()) {
733
- if (now - stat3.ctimeMs < 3e3) return null;
734
- return [stat3.ino, stat3.ctimeMs, stat3.size].map((n) => n.toString(36)).join("-");
735
- }
736
- function createWriteToken(requestToken) {
737
- const token = requestToken?.trim();
738
- return token && token.length <= 200 ? token : randomUUID2();
739
- }
740
- function recordFileWriteReceipt(filePath, receipt) {
741
- const absPath = realFilePath(filePath);
742
- const now = Date.now();
743
- for (const [path, list] of receipts) {
744
- const live = list.filter((entry) => now - entry.recordedAt < RECEIPT_TTL_MS);
745
- if (live.length > 0) receipts.set(path, live);
746
- else receipts.delete(path);
747
- }
748
- receipts.set(absPath, [...receipts.get(absPath) ?? [], { ...receipt, recordedAt: now }]);
749
- }
750
- function clearFileWriteReceipt(filePath, version, writeToken) {
751
- const absPath = realFilePath(filePath);
752
- const current = (receipts.get(absPath) ?? []).filter(
753
- (entry) => entry.version !== version || entry.writeToken !== writeToken
754
- );
755
- if (current.length > 0) receipts.set(absPath, current);
756
- else receipts.delete(absPath);
757
- }
758
- function identifyFileWrite(filePath, expectedVersion) {
759
- const receipt = newestReceipt(realFilePath(filePath), expectedVersion);
760
- if (!receipt) return null;
761
- const { path, version, writeToken } = receipt;
762
- return { path, version, writeToken };
763
- }
764
- function bytesOverwrittenBy(filePath, version) {
765
- return newestReceipt(realFilePath(filePath), version)?.overwrote;
766
- }
767
- function forgetOverwrittenBytes(filePath, versions) {
768
- for (const receipt of receipts.get(realFilePath(filePath)) ?? [])
769
- if (versions.has(receipt.version)) delete receipt.overwrote;
770
- }
771
- function newestReceipt(absPath, expectedVersion) {
772
- const now = Date.now();
773
- const current = (receipts.get(absPath) ?? []).filter(
774
- (entry) => now - entry.recordedAt < RECEIPT_TTL_MS
775
- );
776
- if (current.length > 0) receipts.set(absPath, current);
777
- else receipts.delete(absPath);
778
- let receipt;
779
- for (let i = current.length - 1; i >= 0 && !receipt; i -= 1) {
780
- if (current[i]?.version === expectedVersion) receipt = current[i];
781
- }
782
- return receipt;
783
- }
784
- var consumeFileWriteReceipt = identifyFileWrite;
785
-
786
470
  // src/helpers/applyFileMutations.ts
787
- import { readFileSync as readFileSync4, statSync as statSync2 } from "fs";
788
- function applyFileMutations(projectDir, mutations, requestToken, writeFile2 = (path, content) => replaceFileAtomically(path, content, statSync2(path).mode)) {
471
+ import { readFileSync as readFileSync2, statSync as statSync2 } from "fs";
472
+ function applyFileMutations(projectDir, mutations, requestToken, writeFile = (path, content) => replaceFileAtomically(path, content, statSync2(path).mode)) {
789
473
  const prepared = mutations.map((mutation) => ({
790
474
  ...mutation,
791
- before: mutation.before ?? readFileSync4(mutation.absPath, "utf-8")
475
+ before: mutation.before ?? readFileSync2(mutation.absPath, "utf-8")
792
476
  }));
793
477
  const results = [];
794
478
  const attempted = [];
795
479
  for (const mutation of prepared)
796
- assertExpectedVersion(mutation, readFileSync4(mutation.absPath, "utf-8"));
480
+ assertExpectedVersion(mutation, readFileSync2(mutation.absPath, "utf-8"));
797
481
  try {
798
482
  for (const mutation of prepared) {
799
- results.push(applyOneMutation(projectDir, mutation, requestToken, writeFile2, attempted));
483
+ results.push(applyOneMutation(projectDir, mutation, requestToken, writeFile, attempted));
800
484
  }
801
485
  return results;
802
486
  } catch (error) {
803
487
  const rollbackErrors = [];
804
488
  for (const mutation of attempted.reverse()) {
805
489
  try {
806
- if (!mutation.written || readFileSync4(mutation.absPath, "utf-8") === mutation.after)
807
- writeFile2(mutation.absPath, mutation.before, "utf-8");
490
+ if (!mutation.written || readFileSync2(mutation.absPath, "utf-8") === mutation.after)
491
+ writeFile(mutation.absPath, mutation.before, "utf-8");
808
492
  clearFileWriteReceipt(mutation.absPath, mutation.version, mutation.writeToken);
809
493
  } catch (rollbackError) {
810
494
  rollbackErrors.push(rollbackError);
@@ -819,8 +503,8 @@ function applyFileMutations(projectDir, mutations, requestToken, writeFile2 = (p
819
503
  throw error;
820
504
  }
821
505
  }
822
- function applyOneMutation(projectDir, mutation, requestToken, writeFile2, attempted) {
823
- const current = readFileSync4(mutation.absPath, "utf-8");
506
+ function applyOneMutation(projectDir, mutation, requestToken, writeFile, attempted) {
507
+ const current = readFileSync2(mutation.absPath, "utf-8");
824
508
  assertExpectedVersion(mutation, current);
825
509
  if (mutation.after === mutation.before) {
826
510
  return {
@@ -834,13 +518,13 @@ function applyOneMutation(projectDir, mutation, requestToken, writeFile2, attemp
834
518
  }
835
519
  const backup = snapshotBeforeWrite(projectDir, mutation.absPath);
836
520
  if (backup.error) throw new Error(`backup failed: ${backup.error}`);
837
- assertExpectedVersion(mutation, readFileSync4(mutation.absPath, "utf-8"));
521
+ assertExpectedVersion(mutation, readFileSync2(mutation.absPath, "utf-8"));
838
522
  const before = current;
839
523
  const version = fileContentVersion(mutation.after);
840
524
  const writeToken = createWriteToken(requestToken);
841
525
  const attempt = { ...mutation, before, version, writeToken, written: false };
842
526
  attempted.push(attempt);
843
- writeFile2(mutation.absPath, mutation.after, "utf-8");
527
+ writeFile(mutation.absPath, mutation.after, "utf-8");
844
528
  attempt.written = true;
845
529
  recordFileWriteReceipt(mutation.absPath, {
846
530
  path: mutation.sourceFile,
@@ -902,12 +586,12 @@ import {
902
586
  syncPositionHoldsBeforeKeyframes
903
587
  } from "@hyperframes/parsers/gsap-writer-acorn";
904
588
  import { parseHTML as parseHTML2 } from "linkedom";
905
- import { ensureHfIds as ensureHfIds2 } from "@hyperframes/parsers/hf-ids";
589
+ import { ensureHfIds } from "@hyperframes/parsers/hf-ids";
906
590
 
907
591
  // src/helpers/compositionInsertion.ts
908
- import { existsSync as existsSync2, readFileSync as readFileSync5 } from "fs";
909
- import { randomUUID as randomUUID3 } from "crypto";
910
- import { dirname as dirname3, relative as relative3, resolve as resolve2, sep as sep2 } from "path";
592
+ import { existsSync as existsSync2, readFileSync as readFileSync3 } from "fs";
593
+ import { randomUUID } from "crypto";
594
+ import { dirname as dirname2, relative as relative2, resolve, sep } from "path";
911
595
  import { parseHTML } from "linkedom";
912
596
  import { ProjectRootMissingError } from "@hyperframes/core";
913
597
  var CompositionInsertionError = class extends Error {
@@ -964,7 +648,7 @@ function canonicalDependency(projectDir, ownerAbs, sourcePath) {
964
648
  validateSourcePath(sourcePath);
965
649
  return canonicalProjectPath(
966
650
  projectDir,
967
- resolveWithinProject(projectDir, relative3(projectDir, resolve2(dirname3(ownerAbs), sourcePath)))
651
+ resolveWithinProject(projectDir, relative2(projectDir, resolve(dirname2(ownerAbs), sourcePath)))
968
652
  );
969
653
  }
970
654
  function validateDependencyGraph(projectDir, targetAbs, sourceAbs) {
@@ -979,7 +663,7 @@ function validateDependencyGraph(projectDir, targetAbs, sourceAbs) {
979
663
  }
980
664
  if (visited.has(file)) return;
981
665
  visiting.add(file);
982
- const source = readFileSync5(file, "utf-8");
666
+ const source = readFileSync3(file, "utf-8");
983
667
  const { document } = compositionRoot(source);
984
668
  for (const host of descendants(document, "[data-composition-src]")) {
985
669
  const dependency = host.getAttribute("data-composition-src");
@@ -1037,13 +721,13 @@ function uniqueHostId(root, base) {
1037
721
  return `${base}_${suffix}`;
1038
722
  }
1039
723
  function relativeSourcePath(targetAbs, sourceAbs) {
1040
- return relative3(dirname3(targetAbs), sourceAbs).split(sep2).join("/");
724
+ return relative2(dirname2(targetAbs), sourceAbs).split(sep).join("/");
1041
725
  }
1042
726
  function insertCompositionIntoSource(input) {
1043
727
  const targetAbs = canonicalProjectFile(input.projectDir, input.targetPath);
1044
728
  const sourceAbs = canonicalProjectFile(input.projectDir, input.sourcePath);
1045
729
  validateDependencyGraph(input.projectDir, targetAbs, sourceAbs);
1046
- const source = readFileSync5(sourceAbs, "utf-8");
730
+ const source = readFileSync3(sourceAbs, "utf-8");
1047
731
  const sourceComposition = compositionRoot(source).root;
1048
732
  const duration = positiveAttribute(
1049
733
  sourceComposition,
@@ -1072,7 +756,7 @@ function insertCompositionIntoSource(input) {
1072
756
  const host = document.createElement("div");
1073
757
  host.id = hostId;
1074
758
  host.className = "clip";
1075
- host.setAttribute("data-hf-id", `hf-${randomUUID3()}`);
759
+ host.setAttribute("data-hf-id", `hf-${randomUUID()}`);
1076
760
  host.setAttribute("data-composition-id", hostId);
1077
761
  host.setAttribute("data-composition-src", relativeSourcePath(targetAbs, sourceAbs));
1078
762
  host.setAttribute("data-start", String(Math.round(input.start * 100) / 100));
@@ -1169,15 +853,15 @@ async function loadGsapParser() {
1169
853
  return import("@hyperframes/parsers/gsap-parser-recast");
1170
854
  }
1171
855
  function isDanglingSymlinkInProject(projectDir, lexicalPath) {
1172
- if (!isSafePath(projectDir, dirname4(lexicalPath))) return false;
856
+ if (!isSafePath(projectDir, dirname3(lexicalPath))) return false;
1173
857
  let stats;
1174
858
  try {
1175
- stats = lstatSync3(lexicalPath);
859
+ stats = lstatSync2(lexicalPath);
1176
860
  } catch {
1177
861
  return false;
1178
862
  }
1179
863
  if (!stats.isSymbolicLink()) return false;
1180
- const target = resolve3(dirname4(lexicalPath), readlinkSync(lexicalPath));
864
+ const target = resolve2(dirname3(lexicalPath), readlinkSync(lexicalPath));
1181
865
  if (!isSafePath(projectDir, target)) return false;
1182
866
  try {
1183
867
  statSync3(lexicalPath);
@@ -1201,7 +885,7 @@ async function resolveProjectPath(c, adapter, route, opts) {
1201
885
  }
1202
886
  const absPath = (opts?.pin ? pinWithinProject : resolveWithinProject)(project.dir, filePath);
1203
887
  if (!absPath) {
1204
- if (isDanglingSymlinkInProject(project.dir, resolve3(project.dir, filePath))) {
888
+ if (isDanglingSymlinkInProject(project.dir, resolve2(project.dir, filePath))) {
1205
889
  return { error: c.json({ error: "not found", why: "dangling_symlink" }, 404) };
1206
890
  }
1207
891
  return { error: c.json({ error: "forbidden", why: "outside_project" }, 403) };
@@ -1265,10 +949,10 @@ function foldElementPatches(originalContent, patches) {
1265
949
  return { content, matched };
1266
950
  }
1267
951
  var PATCH_CONFLICT_ATTEMPTS = 3;
1268
- function commitElementPatchBatches(projectDir, batches, writeFile2 = (path, content) => replaceFileAtomically(path, content, statSync3(path).mode), requestToken) {
952
+ function commitElementPatchBatches(projectDir, batches, writeFile = (path, content) => replaceFileAtomically(path, content, statSync3(path).mode), requestToken) {
1269
953
  for (let attempt = 1; ; attempt += 1) {
1270
954
  try {
1271
- return foldAndCommitElementPatchBatches(projectDir, batches, writeFile2, requestToken);
955
+ return foldAndCommitElementPatchBatches(projectDir, batches, writeFile, requestToken);
1272
956
  } catch (error) {
1273
957
  if (!(error instanceof FileChangedError)) throw error;
1274
958
  if (attempt === PATCH_CONFLICT_ATTEMPTS)
@@ -1276,7 +960,7 @@ function commitElementPatchBatches(projectDir, batches, writeFile2 = (path, cont
1276
960
  }
1277
961
  }
1278
962
  }
1279
- function foldAndCommitElementPatchBatches(projectDir, batches, writeFile2, requestToken) {
963
+ function foldAndCommitElementPatchBatches(projectDir, batches, writeFile, requestToken) {
1280
964
  const resolvedPaths = /* @__PURE__ */ new Set();
1281
965
  const prepared = [];
1282
966
  for (const batch of batches) {
@@ -1286,7 +970,7 @@ function foldAndCommitElementPatchBatches(projectDir, batches, writeFile2, reque
1286
970
  resolvedPaths.add(absPath);
1287
971
  let before;
1288
972
  try {
1289
- before = readFileSync6(absPath, "utf-8");
973
+ before = readFileSync4(absPath, "utf-8");
1290
974
  } catch {
1291
975
  return { error: "not-found", sourceFile: batch.sourceFile };
1292
976
  }
@@ -1321,7 +1005,7 @@ function foldAndCommitElementPatchBatches(projectDir, batches, writeFile2, reque
1321
1005
  after
1322
1006
  })),
1323
1007
  requestToken,
1324
- writeFile2
1008
+ writeFile
1325
1009
  );
1326
1010
  const files = applied.map((file, index) => ({
1327
1011
  sourceFile: file.sourceFile,
@@ -1342,7 +1026,7 @@ function commitElementPatchBatchesWithReceipts(c, projectDir, batches) {
1342
1026
  );
1343
1027
  }
1344
1028
  function writeFileWithReceipt(c, filePath, absPath, html) {
1345
- const overwrote = readFileSync6(absPath);
1029
+ const overwrote = readFileSync4(absPath);
1346
1030
  replaceFileAtomically(absPath, html, statSync3(absPath).mode);
1347
1031
  const version = fileContentVersion(html);
1348
1032
  const writeToken = createWriteToken(c.req.header("X-Hyperframes-Write-Token"));
@@ -1352,7 +1036,7 @@ function writeFileWithReceipt(c, filePath, absPath, html) {
1352
1036
  function writeMutationResult(c, projectDir, filePath, absPath, html, original) {
1353
1037
  const backup = snapshotBeforeWrite(projectDir, absPath);
1354
1038
  if (backup.error) return c.json({ error: `backup failed: ${backup.error}` }, 500);
1355
- if (readFileSync6(absPath, "utf-8") !== original) {
1039
+ if (readFileSync4(absPath, "utf-8") !== original) {
1356
1040
  return c.json({ error: "file changed", conflict: true, path: filePath }, 409);
1357
1041
  }
1358
1042
  const { version } = writeFileWithReceipt(c, filePath, absPath, html);
@@ -1398,7 +1082,7 @@ async function parseMutationBody(c) {
1398
1082
  return { target: body.target, body };
1399
1083
  }
1400
1084
  function ensureDir(projectDir, filePath) {
1401
- mkdirWithinProject(projectDir, dirname4(filePath));
1085
+ mkdirWithinProject(projectDir, dirname3(filePath));
1402
1086
  }
1403
1087
  function generateCopyPath(projectDir, originalPath) {
1404
1088
  const ext = originalPath.includes(".") ? "." + originalPath.split(".").pop() : "";
@@ -1407,7 +1091,7 @@ function generateCopyPath(projectDir, originalPath) {
1407
1091
  const cleanBase = copyMatch ? base.slice(0, -copyMatch[0].length) : base;
1408
1092
  let num = copyMatch ? copyMatch[1] ? parseInt(copyMatch[1]) + 1 : 2 : 1;
1409
1093
  let candidate = num === 1 ? `${cleanBase} (copy)${ext}` : `${cleanBase} (copy ${num})${ext}`;
1410
- while (existsSync3(resolve3(projectDir, candidate))) {
1094
+ while (existsSync3(resolve2(projectDir, candidate))) {
1411
1095
  num++;
1412
1096
  candidate = `${cleanBase} (copy ${num})${ext}`;
1413
1097
  }
@@ -1415,8 +1099,8 @@ function generateCopyPath(projectDir, originalPath) {
1415
1099
  }
1416
1100
  function walkFiles(dir, filter) {
1417
1101
  const results = [];
1418
- for (const entry of readdirSync3(dir, { withFileTypes: true })) {
1419
- const full = join6(dir, entry.name);
1102
+ for (const entry of readdirSync2(dir, { withFileTypes: true })) {
1103
+ const full = join5(dir, entry.name);
1420
1104
  if (entry.isDirectory()) {
1421
1105
  if (entry.name === "node_modules" || entry.name === ".thumbnails" || entry.name === "renders" || entry.name === ".transcode-cache")
1422
1106
  continue;
@@ -1435,7 +1119,7 @@ function updateReferences(projectDir, oldPath, newPath) {
1435
1119
  let updatedCount = 0;
1436
1120
  for (const file of textFiles) {
1437
1121
  if (!isSafePath(projectDir, file)) continue;
1438
- const content = readFileSync6(file, "utf-8");
1122
+ const content = readFileSync4(file, "utf-8");
1439
1123
  if (!content.includes(oldPath)) continue;
1440
1124
  const updated = content.split(oldPath).join(newPath);
1441
1125
  if (updated !== content) {
@@ -1446,7 +1130,7 @@ function updateReferences(projectDir, oldPath, newPath) {
1446
1130
  return updatedCount;
1447
1131
  }
1448
1132
  function extractGsapScriptBlock(html) {
1449
- const { document } = parseHTML2(ensureHfIds2(html));
1133
+ const { document } = parseHTML2(ensureHfIds(html));
1450
1134
  const scripts = [
1451
1135
  ...document.querySelectorAll("script:not([src])"),
1452
1136
  ...Array.from(document.querySelectorAll("template")).flatMap(
@@ -1709,7 +1393,7 @@ function containsRawGsapExpression(value) {
1709
1393
  return Object.values(value).some(containsRawGsapExpression);
1710
1394
  }
1711
1395
  async function prepareGsapMutationScript(c, res, firstMutation) {
1712
- const beforeHtml = readFileSync6(res.absPath, "utf-8");
1396
+ const beforeHtml = readFileSync4(res.absPath, "utf-8");
1713
1397
  let html = beforeHtml;
1714
1398
  let block = extractGsapScriptBlock(html);
1715
1399
  if (!block && (firstMutation.type === "add" || firstMutation.type === "add-with-keyframes")) {
@@ -1776,7 +1460,7 @@ async function applyGsapMutations(c, res, mutations) {
1776
1460
  const changed = block.scriptText !== initialScript;
1777
1461
  const newHtml = changed ? block.replaceScript(block.scriptText) : html;
1778
1462
  let backupPath = null;
1779
- if (readFileSync6(res.absPath, "utf-8") !== beforeHtml) {
1463
+ if (readFileSync4(res.absPath, "utf-8") !== beforeHtml) {
1780
1464
  return c.json({ error: "file changed during GSAP mutation", conflict: true }, 409);
1781
1465
  }
1782
1466
  if (changed) {
@@ -2530,7 +2214,7 @@ async function processUploadedFiles(formData, targetDir, projectDir) {
2530
2214
  skipped.push(name);
2531
2215
  continue;
2532
2216
  }
2533
- const destPath = resolve3(targetDir, name);
2217
+ const destPath = resolve2(targetDir, name);
2534
2218
  if (!isSafePath(projectDir, destPath)) continue;
2535
2219
  let finalPath = destPath;
2536
2220
  let finalName = name;
@@ -2541,13 +2225,13 @@ async function processUploadedFiles(formData, targetDir, projectDir) {
2541
2225
  let n = 1;
2542
2226
  if (existsSync3(finalPath)) {
2543
2227
  n = 2;
2544
- while (n < MAX_COPY_INDEX && existsSync3(resolve3(targetDir, `${base} (${n})${ext}`))) n++;
2228
+ while (n < MAX_COPY_INDEX && existsSync3(resolve2(targetDir, `${base} (${n})${ext}`))) n++;
2545
2229
  if (n >= MAX_COPY_INDEX) {
2546
2230
  skipped.push(name);
2547
2231
  continue;
2548
2232
  }
2549
2233
  finalName = `${base} (${n})${ext}`;
2550
- finalPath = resolve3(targetDir, finalName);
2234
+ finalPath = resolve2(targetDir, finalName);
2551
2235
  }
2552
2236
  if (!isSafePath(projectDir, finalPath)) continue;
2553
2237
  const buffer = Buffer.from(await value.arrayBuffer());
@@ -2559,11 +2243,11 @@ async function processUploadedFiles(formData, targetDir, projectDir) {
2559
2243
  let written = false;
2560
2244
  while (n < MAX_COPY_INDEX && isSafePath(projectDir, finalPath)) {
2561
2245
  try {
2562
- const fd = openSync2(finalPath, "wx");
2246
+ const fd = openSync(finalPath, "wx");
2563
2247
  try {
2564
- writeFileSync5(fd, buffer);
2248
+ writeFileSync4(fd, buffer);
2565
2249
  } finally {
2566
- closeSync2(fd);
2250
+ closeSync(fd);
2567
2251
  }
2568
2252
  written = true;
2569
2253
  break;
@@ -2573,14 +2257,14 @@ async function processUploadedFiles(formData, targetDir, projectDir) {
2573
2257
  }
2574
2258
  n++;
2575
2259
  finalName = `${base} (${n})${ext}`;
2576
- finalPath = resolve3(targetDir, finalName);
2260
+ finalPath = resolve2(targetDir, finalName);
2577
2261
  }
2578
2262
  }
2579
2263
  if (!written) {
2580
2264
  if (n >= MAX_COPY_INDEX) skipped.push(name);
2581
2265
  continue;
2582
2266
  }
2583
- const relativePath = subDir ? join6(subDir, finalName) : finalName;
2267
+ const relativePath = subDir ? join5(subDir, finalName) : finalName;
2584
2268
  uploaded.push(relativePath);
2585
2269
  if (validation.unchecked) unchecked.push({ name: finalName, reason: validation.unchecked });
2586
2270
  if (isAudioFile(finalName)) {
@@ -2596,7 +2280,7 @@ function registerFileRoutes(api, adapter) {
2596
2280
  if ("error" in res) return res.error;
2597
2281
  let fd;
2598
2282
  try {
2599
- fd = openSync2(res.absPath, "r");
2283
+ fd = openSync(res.absPath, "r");
2600
2284
  } catch {
2601
2285
  if (c.req.query("optional") === "1") {
2602
2286
  return c.json({ filename: res.filePath, content: "", missing: true });
@@ -2604,10 +2288,10 @@ function registerFileRoutes(api, adapter) {
2604
2288
  return c.json({ error: "not found" }, 404);
2605
2289
  }
2606
2290
  try {
2607
- if (!fstatSync2(fd).isFile()) {
2291
+ if (!fstatSync(fd).isFile()) {
2608
2292
  return c.json({ error: "not found", why: "not_a_file" }, 404);
2609
2293
  }
2610
- const content = readFileSync6(fd);
2294
+ const content = readFileSync4(fd);
2611
2295
  const version = fileContentVersion(content);
2612
2296
  c.header("ETag", version);
2613
2297
  return c.json({
@@ -2617,7 +2301,7 @@ function registerFileRoutes(api, adapter) {
2617
2301
  missing: false
2618
2302
  });
2619
2303
  } finally {
2620
- closeSync2(fd);
2304
+ closeSync(fd);
2621
2305
  }
2622
2306
  });
2623
2307
  api.put("/projects/:id/files/*", async (c) => {
@@ -2629,7 +2313,7 @@ function registerFileRoutes(api, adapter) {
2629
2313
  if (expectedVersion === null && !createOnly) {
2630
2314
  let currentContent = null;
2631
2315
  try {
2632
- currentContent = readFileSync6(res.absPath);
2316
+ currentContent = readFileSync4(res.absPath);
2633
2317
  } catch (error) {
2634
2318
  if (!error || typeof error !== "object" || !("code" in error) || error.code !== "ENOENT") {
2635
2319
  throw error;
@@ -2651,12 +2335,12 @@ function registerFileRoutes(api, adapter) {
2651
2335
  ensureDir(res.project.dir, res.absPath);
2652
2336
  let fd;
2653
2337
  try {
2654
- fd = openSync2(res.absPath, "wx");
2338
+ fd = openSync(res.absPath, "wx");
2655
2339
  } catch (error) {
2656
2340
  if (!error || typeof error !== "object" || !("code" in error) || error.code !== "EEXIST") {
2657
2341
  throw error;
2658
2342
  }
2659
- const currentContent = readFileSync6(res.absPath);
2343
+ const currentContent = readFileSync4(res.absPath);
2660
2344
  return c.json(
2661
2345
  {
2662
2346
  error: "file conflict",
@@ -2670,12 +2354,12 @@ function registerFileRoutes(api, adapter) {
2670
2354
  try {
2671
2355
  writeSync(fd, body, 0, body.length, 0);
2672
2356
  } finally {
2673
- closeSync2(fd);
2357
+ closeSync(fd);
2674
2358
  }
2675
2359
  } else {
2676
2360
  let fd;
2677
2361
  try {
2678
- fd = openSync2(res.absPath, "r+");
2362
+ fd = openSync(res.absPath, "r+");
2679
2363
  } catch (error) {
2680
2364
  if (!error || typeof error !== "object" || !("code" in error) || error.code !== "ENOENT") {
2681
2365
  throw error;
@@ -2691,7 +2375,7 @@ function registerFileRoutes(api, adapter) {
2691
2375
  );
2692
2376
  }
2693
2377
  try {
2694
- const currentContent = readFileSync6(fd);
2378
+ const currentContent = readFileSync4(fd);
2695
2379
  overwrote = currentContent;
2696
2380
  const currentVersion = fileContentVersion(currentContent);
2697
2381
  if (expectedVersion !== currentVersion) {
@@ -2707,12 +2391,12 @@ function registerFileRoutes(api, adapter) {
2707
2391
  }
2708
2392
  backup = snapshotBeforeWrite(res.project.dir, res.absPath);
2709
2393
  if (backup.error) return c.json({ error: `backup failed: ${backup.error}` }, 500);
2710
- const mode = fstatSync2(fd).mode;
2711
- closeSync2(fd);
2394
+ const mode = fstatSync(fd).mode;
2395
+ closeSync(fd);
2712
2396
  fd = null;
2713
2397
  replaceFileAtomically(res.absPath, body, mode);
2714
2398
  } finally {
2715
- if (fd !== null) closeSync2(fd);
2399
+ if (fd !== null) closeSync(fd);
2716
2400
  }
2717
2401
  }
2718
2402
  const version = fileContentVersion(body);
@@ -2733,7 +2417,7 @@ function registerFileRoutes(api, adapter) {
2733
2417
  ensureDir(res.project.dir, res.absPath);
2734
2418
  const body = Buffer.from(await c.req.arrayBuffer());
2735
2419
  try {
2736
- writeFileSync5(res.absPath, body, { flag: "wx" });
2420
+ writeFileSync4(res.absPath, body, { flag: "wx" });
2737
2421
  } catch (error) {
2738
2422
  if (!error || typeof error !== "object" || !("code" in error) || error.code !== "EEXIST") {
2739
2423
  throw error;
@@ -2745,10 +2429,10 @@ function registerFileRoutes(api, adapter) {
2745
2429
  api.delete("/projects/:id/files/*", async (c) => {
2746
2430
  const res = await resolveProjectFile(c, adapter, { mustExist: true });
2747
2431
  if ("error" in res) return res.error;
2748
- const stat3 = statSync3(res.absPath);
2432
+ const stat2 = statSync3(res.absPath);
2749
2433
  const backup = snapshotBeforeWrite(res.project.dir, res.absPath);
2750
2434
  if (backup.error) return c.json({ error: `backup failed: ${backup.error}` }, 500);
2751
- if (stat3.isDirectory()) {
2435
+ if (stat2.isDirectory()) {
2752
2436
  rmSync3(res.absPath, { recursive: true });
2753
2437
  } else {
2754
2438
  unlinkSync2(res.absPath);
@@ -2767,7 +2451,7 @@ function registerFileRoutes(api, adapter) {
2767
2451
  }
2768
2452
  let before;
2769
2453
  try {
2770
- before = readFileSync6(ctx.absPath, "utf-8");
2454
+ before = readFileSync4(ctx.absPath, "utf-8");
2771
2455
  } catch (error) {
2772
2456
  if (!error || typeof error !== "object" || !("code" in error) || error.code !== "ENOENT") {
2773
2457
  throw error;
@@ -2796,7 +2480,7 @@ function registerFileRoutes(api, adapter) {
2796
2480
  }
2797
2481
  const backup = snapshotBeforeWrite(ctx.project.dir, ctx.absPath);
2798
2482
  if (backup.error) return c.json({ error: `backup failed: ${backup.error}` }, 500);
2799
- const current = readFileSync6(ctx.absPath, "utf-8");
2483
+ const current = readFileSync4(ctx.absPath, "utf-8");
2800
2484
  if (current !== before) {
2801
2485
  const currentVersion2 = fileContentVersion(current);
2802
2486
  return c.json({ error: "file conflict", currentVersion: currentVersion2, currentContent: current }, 409);
@@ -2829,7 +2513,7 @@ function registerFileRoutes(api, adapter) {
2829
2513
  }
2830
2514
  const parsed = await parseMutationBody(c);
2831
2515
  if ("error" in parsed) return parsed.error;
2832
- const originalContent = readFileSync6(ctx.absPath, "utf-8");
2516
+ const originalContent = readFileSync4(ctx.absPath, "utf-8");
2833
2517
  return writeIfChanged(
2834
2518
  c,
2835
2519
  ctx.project.dir,
@@ -2850,7 +2534,7 @@ function registerFileRoutes(api, adapter) {
2850
2534
  if (!Array.isArray(targets) || targets.length === 0) {
2851
2535
  return c.json({ error: "targets required" }, 400);
2852
2536
  }
2853
- const originalContent = readFileSync6(ctx.absPath, "utf-8");
2537
+ const originalContent = readFileSync4(ctx.absPath, "utf-8");
2854
2538
  let next = originalContent;
2855
2539
  for (const target of targets) {
2856
2540
  next = removeElementFromHtml(next, target);
@@ -2883,7 +2567,7 @@ function registerFileRoutes(api, adapter) {
2883
2567
  seen.add(absPath);
2884
2568
  let before;
2885
2569
  try {
2886
- before = readFileSync6(absPath, "utf-8");
2570
+ before = readFileSync4(absPath, "utf-8");
2887
2571
  } catch {
2888
2572
  return c.json({ error: `not found: ${file.path}` }, 404);
2889
2573
  }
@@ -2910,7 +2594,7 @@ function registerFileRoutes(api, adapter) {
2910
2594
  prepared.push(folded);
2911
2595
  }
2912
2596
  for (const file of prepared) {
2913
- const current = readFileSync6(file.absPath, "utf-8");
2597
+ const current = readFileSync4(file.absPath, "utf-8");
2914
2598
  if (current !== file.before) {
2915
2599
  return c.json(
2916
2600
  {
@@ -2953,7 +2637,7 @@ function registerFileRoutes(api, adapter) {
2953
2637
  const conflicts = [];
2954
2638
  for (const file of written.reverse()) {
2955
2639
  try {
2956
- const current = readFileSync6(file.absPath, "utf-8");
2640
+ const current = readFileSync4(file.absPath, "utf-8");
2957
2641
  if (current !== file.after) {
2958
2642
  conflicts.push(file.path);
2959
2643
  continue;
@@ -3001,7 +2685,7 @@ function registerFileRoutes(api, adapter) {
3001
2685
  const fallbackTiming = typeof parsed.body.elementStart === "number" && typeof parsed.body.elementDuration === "number" ? { start: parsed.body.elementStart, duration: parsed.body.elementDuration } : void 0;
3002
2686
  let originalContent;
3003
2687
  try {
3004
- originalContent = readFileSync6(ctx.absPath, "utf-8");
2688
+ originalContent = readFileSync4(ctx.absPath, "utf-8");
3005
2689
  } catch {
3006
2690
  return c.json({ error: "not found" }, 404);
3007
2691
  }
@@ -3059,7 +2743,7 @@ function registerFileRoutes(api, adapter) {
3059
2743
  for (let attempt = 1; ; attempt += 1) {
3060
2744
  let originalContent;
3061
2745
  try {
3062
- originalContent = readFileSync6(ctx.absPath, "utf-8");
2746
+ originalContent = readFileSync4(ctx.absPath, "utf-8");
3063
2747
  } catch {
3064
2748
  return c.json({ error: "not found" }, 404);
3065
2749
  }
@@ -3165,7 +2849,7 @@ function registerFileRoutes(api, adapter) {
3165
2849
  }
3166
2850
  let originalContent;
3167
2851
  try {
3168
- originalContent = readFileSync6(ctx.absPath, "utf-8");
2852
+ originalContent = readFileSync4(ctx.absPath, "utf-8");
3169
2853
  } catch {
3170
2854
  return c.json({ error: "not found" }, 404);
3171
2855
  }
@@ -3221,7 +2905,7 @@ function registerFileRoutes(api, adapter) {
3221
2905
  ).map((entry) => ({ target: entry.target, track: entry.track }));
3222
2906
  let originalContent;
3223
2907
  try {
3224
- originalContent = readFileSync6(ctx.absPath, "utf-8");
2908
+ originalContent = readFileSync4(ctx.absPath, "utf-8");
3225
2909
  } catch {
3226
2910
  return c.json({ error: "not found" }, 404);
3227
2911
  }
@@ -3250,7 +2934,7 @@ function registerFileRoutes(api, adapter) {
3250
2934
  if ("error" in parsed) return parsed.error;
3251
2935
  let content;
3252
2936
  try {
3253
- content = readFileSync6(ctx.absPath, "utf-8");
2937
+ content = readFileSync4(ctx.absPath, "utf-8");
3254
2938
  } catch {
3255
2939
  return c.json({ exists: false });
3256
2940
  }
@@ -3294,7 +2978,7 @@ function registerFileRoutes(api, adapter) {
3294
2978
  }
3295
2979
  ensureDir(project.dir, destAbs);
3296
2980
  try {
3297
- writeFileSync5(destAbs, readFileSync6(srcAbs), { flag: "wx" });
2981
+ writeFileSync4(destAbs, readFileSync4(srcAbs), { flag: "wx" });
3298
2982
  } catch (error) {
3299
2983
  if (!error || typeof error !== "object" || !("code" in error) || error.code !== "EEXIST") {
3300
2984
  throw error;
@@ -3338,7 +3022,7 @@ function registerFileRoutes(api, adapter) {
3338
3022
  mustExist: true
3339
3023
  });
3340
3024
  if ("error" in res) return res.error;
3341
- const html = readFileSync6(res.absPath, "utf-8");
3025
+ const html = readFileSync4(res.absPath, "utf-8");
3342
3026
  const block = extractGsapScriptBlock(html);
3343
3027
  if (!block) {
3344
3028
  return c.json({
@@ -3394,7 +3078,7 @@ function registerFileRoutes(api, adapter) {
3394
3078
  if (!body || typeof body.expected !== "string" || typeof body.restore !== "string") {
3395
3079
  return c.json({ error: "expected and restore contents required" }, 400);
3396
3080
  }
3397
- const current = readFileSync6(res.absPath, "utf-8");
3081
+ const current = readFileSync4(res.absPath, "utf-8");
3398
3082
  if (current !== body.expected) {
3399
3083
  return c.json({ ok: true, restored: false, conflict: true });
3400
3084
  }
@@ -3404,10 +3088,10 @@ function registerFileRoutes(api, adapter) {
3404
3088
  }
3405
3089
 
3406
3090
  // src/routes/preview.ts
3407
- import { createReadStream, existsSync as existsSync5, readFileSync as readFileSync8, statSync as statSync4 } from "fs";
3091
+ import { createReadStream, existsSync as existsSync5, readFileSync as readFileSync6, statSync as statSync4 } from "fs";
3408
3092
  import { Readable } from "stream";
3409
- import { join as join9, resolve as resolve5 } from "path";
3410
- import { createHash as createHash4 } from "crypto";
3093
+ import { join as join8, resolve as resolve4 } from "path";
3094
+ import { createHash } from "crypto";
3411
3095
  import {
3412
3096
  addScenePartsManifest,
3413
3097
  injectScriptsIntoHtml,
@@ -3419,8 +3103,8 @@ import { injectTagsAtHeadStart } from "@hyperframes/core/compiler/html-document"
3419
3103
  import { isWithinProjectRoot } from "@hyperframes/parsers/asset-resolution";
3420
3104
 
3421
3105
  // src/helpers/subComposition.ts
3422
- import { existsSync as existsSync4, readFileSync as readFileSync7 } from "fs";
3423
- import { join as join7 } from "path";
3106
+ import { existsSync as existsSync4, readFileSync as readFileSync5 } from "fs";
3107
+ import { join as join6 } from "path";
3424
3108
  import { parseHTML as parseHTML3 } from "linkedom";
3425
3109
  import {
3426
3110
  rewriteAssetPaths,
@@ -3430,7 +3114,7 @@ import {
3430
3114
  import { stripEmbeddedRuntimeScripts } from "@hyperframes/core/compiler";
3431
3115
  import { isFullHtmlDocument } from "@hyperframes/core/compiler/html-document";
3432
3116
  function rewriteRelativePaths(root, compPath, projectDir) {
3433
- const assetExists = (path) => existsSync4(join7(projectDir, path));
3117
+ const assetExists = (path) => existsSync4(join6(projectDir, path));
3434
3118
  rewriteAssetPaths(
3435
3119
  root.querySelectorAll("[src], [href]"),
3436
3120
  compPath,
@@ -3527,9 +3211,9 @@ function tagRootCompositionFile(bodyHtml, compPath) {
3527
3211
  return bodyHtml.slice(0, tagEnd) + ` data-composition-file="${compPath}"` + bodyHtml.slice(tagEnd);
3528
3212
  }
3529
3213
  function buildSubCompositionHtml(projectDir, compPath, runtimeUrl, baseHref, rawOverride) {
3530
- const compFile = join7(projectDir, compPath);
3214
+ const compFile = join6(projectDir, compPath);
3531
3215
  if (!existsSync4(compFile)) return null;
3532
- const rawComp = rawOverride ?? readFileSync7(compFile, "utf-8");
3216
+ const rawComp = rawOverride ?? readFileSync5(compFile, "utf-8");
3533
3217
  let compHeadContent = "";
3534
3218
  let rewrittenContent;
3535
3219
  let htmlAttrs = "";
@@ -3559,10 +3243,10 @@ function buildSubCompositionHtml(projectDir, compPath, runtimeUrl, baseHref, raw
3559
3243
  }
3560
3244
  rewrittenContent = stripEmbeddedRuntimeScripts(rewrittenContent);
3561
3245
  rewrittenContent = tagRootCompositionFile(rewrittenContent, compPath);
3562
- const indexPath = join7(projectDir, "index.html");
3246
+ const indexPath = join6(projectDir, "index.html");
3563
3247
  let headContent = "";
3564
3248
  if (existsSync4(indexPath)) {
3565
- const indexHtml = readFileSync7(indexPath, "utf-8");
3249
+ const indexHtml = readFileSync5(indexPath, "utf-8");
3566
3250
  const headMatch = indexHtml.match(/<head[^>]*>([\s\S]*?)<\/head>/i);
3567
3251
  headContent = headMatch?.[1] ?? "";
3568
3252
  }
@@ -3599,18 +3283,18 @@ function hasBaseElement(html) {
3599
3283
  }
3600
3284
 
3601
3285
  // src/routes/preview.ts
3602
- import { ensureHfIds as ensureHfIds3 } from "@hyperframes/parsers/hf-ids";
3286
+ import { ensureHfIds as ensureHfIds2 } from "@hyperframes/parsers/hf-ids";
3603
3287
 
3604
3288
  // src/helpers/previewReads.ts
3605
3289
  import { readlinkSync as readlinkSync2 } from "fs";
3606
- import { dirname as dirname5, join as join8, parse, relative as relative4, resolve as resolve4 } from "path";
3290
+ import { dirname as dirname4, join as join7, parse, relative as relative3, resolve as resolve3 } from "path";
3607
3291
  var ALWAYS_AFFECTS = ["hyperframes.json", ...STUDIO_SIGNATURE_MANIFEST_PATHS];
3608
3292
  var REFERENCE = /\b(?:src|href|poster|data-composition-src)\s*=\s*(?:"([^"\n]*)"|'([^'\n]*)')|url\(\s*(?:"([^"\n]*)"|'([^'\n]*)'|([^"'\s)]+))/gi;
3609
- var pathKey = process.platform === "darwin" || process.platform === "win32" ? (path) => resolve4(path).toLowerCase() : (path) => resolve4(path);
3293
+ var pathKey = process.platform === "darwin" || process.platform === "win32" ? (path) => resolve3(path).toLowerCase() : (path) => resolve3(path);
3610
3294
  var readsByProject = /* @__PURE__ */ new Map();
3611
3295
  var builtProjects = /* @__PURE__ */ new Set();
3612
- var projectKey = (projectDir) => pathKey(realFilePath(resolve4(projectDir)));
3613
- var inRealProject = (projectDir, path) => join8(realFilePath(resolve4(projectDir)), relative4(resolve4(projectDir), resolve4(projectDir, path)));
3296
+ var projectKey = (projectDir) => pathKey(realFilePath(resolve3(projectDir)));
3297
+ var inRealProject = (projectDir, path) => join7(realFilePath(resolve3(projectDir)), relative3(resolve3(projectDir), resolve3(projectDir, path)));
3614
3298
  var readLink = (path) => {
3615
3299
  try {
3616
3300
  return readlinkSync2(path);
@@ -3626,10 +3310,10 @@ function linksOnTheWay(path) {
3626
3310
  const part = rest.shift();
3627
3311
  if (!part || part === ".") continue;
3628
3312
  if (part === "..") {
3629
- at = dirname5(at);
3313
+ at = dirname4(at);
3630
3314
  continue;
3631
3315
  }
3632
- const next = join8(at, part);
3316
+ const next = join7(at, part);
3633
3317
  const target = hops < 40 ? readLink(next) : null;
3634
3318
  if (target === null) {
3635
3319
  at = next;
@@ -3650,7 +3334,7 @@ function recordPreviewRead(projectDir, filePath) {
3650
3334
  if (!reads) readsByProject.set(key, reads = /* @__PURE__ */ new Set());
3651
3335
  const read = inRealProject(projectDir, filePath);
3652
3336
  for (const found of [read, ...linksOnTheWay(read)]) {
3653
- for (let path = pathKey(found); !reads.has(path); path = dirname5(path)) reads.add(path);
3337
+ for (let path = pathKey(found); !reads.has(path); path = dirname4(path)) reads.add(path);
3654
3338
  }
3655
3339
  }
3656
3340
  function recordPreviewReferences(projectDir, html) {
@@ -3747,10 +3431,10 @@ function injectProjectSignature(html, signature) {
3747
3431
  ${html}`;
3748
3432
  }
3749
3433
  function readStudioMotionManifestContent(projectDir) {
3750
- const manifestPath = join9(projectDir, STUDIO_MOTION_PATH);
3434
+ const manifestPath = join8(projectDir, STUDIO_MOTION_PATH);
3751
3435
  if (!existsSync5(manifestPath)) return "";
3752
3436
  try {
3753
- return readFileSync8(manifestPath, "utf-8");
3437
+ return readFileSync6(manifestPath, "utf-8");
3754
3438
  } catch {
3755
3439
  return "";
3756
3440
  }
@@ -3861,7 +3545,7 @@ function parsePreviewVariablesParam(raw) {
3861
3545
  }
3862
3546
  function variablesEtagSalt(raw) {
3863
3547
  if (!raw) return "";
3864
- return `:vars:${createHash4("sha1").update(raw).digest("hex").slice(0, 12)}`;
3548
+ return `:vars:${createHash("sha1").update(raw).digest("hex").slice(0, 12)}`;
3865
3549
  }
3866
3550
  function previewVariablesFromRequest(rawVariables) {
3867
3551
  const parse2 = parsePreviewVariablesParam(rawVariables);
@@ -3895,19 +3579,19 @@ async function transformPreviewHtml(html, adapter, project, activeCompositionPat
3895
3579
  }
3896
3580
  }
3897
3581
  function resolveProjectMainHtml(projectDir, projectId) {
3898
- const indexPath = join9(projectDir, "index.html");
3899
- const blockHtmlPath = join9(projectDir, `${projectId}.html`);
3582
+ const indexPath = join8(projectDir, "index.html");
3583
+ const blockHtmlPath = join8(projectDir, `${projectId}.html`);
3900
3584
  recordPreviewRead(projectDir, indexPath);
3901
3585
  recordPreviewRead(projectDir, blockHtmlPath);
3902
3586
  if (existsSync5(indexPath)) {
3903
3587
  return {
3904
- html: readFileSync8(indexPath, "utf-8"),
3588
+ html: readFileSync6(indexPath, "utf-8"),
3905
3589
  compositionPath: "index.html"
3906
3590
  };
3907
3591
  }
3908
3592
  if (existsSync5(blockHtmlPath)) {
3909
3593
  return {
3910
- html: readFileSync8(blockHtmlPath, "utf-8"),
3594
+ html: readFileSync6(blockHtmlPath, "utf-8"),
3911
3595
  compositionPath: `${projectId}.html`
3912
3596
  };
3913
3597
  }
@@ -3934,7 +3618,7 @@ function registerPreviewRoutes(api, adapter) {
3934
3618
  const previewBuilds = /* @__PURE__ */ new Map();
3935
3619
  async function buildPreview(project, previewVariables, builtKey, capture) {
3936
3620
  const diskMain = resolveProjectMainHtml(project.dir, project.id);
3937
- const normalizedDisk = diskMain ? ensureHfIds3(diskMain.html) : null;
3621
+ const normalizedDisk = diskMain ? ensureHfIds2(diskMain.html) : null;
3938
3622
  try {
3939
3623
  let bundled = await adapter.bundle(project.dir, {
3940
3624
  stampHfIds: true,
@@ -3959,7 +3643,7 @@ ${runtimeTag}`;
3959
3643
  bundled = bundled.replace(/<head>/i, `<head><base href="${baseHref}">`);
3960
3644
  }
3961
3645
  bundled = injectStudioPreviewAugmentations(
3962
- ensureHfIds3(await transformPreviewHtml(bundled, adapter, project, mainCompositionPath)),
3646
+ ensureHfIds2(await transformPreviewHtml(bundled, adapter, project, mainCompositionPath)),
3963
3647
  adapter,
3964
3648
  project.dir,
3965
3649
  mainCompositionPath,
@@ -3980,7 +3664,7 @@ ${runtimeTag}`;
3980
3664
  } catch {
3981
3665
  const fallback = resolveProjectMainHtml(project.dir, project.id);
3982
3666
  if (fallback) {
3983
- const fallbackHtml = ensureHfIds3(fallback.html);
3667
+ const fallbackHtml = ensureHfIds2(fallback.html);
3984
3668
  let fallbackAugmented = injectStudioPreviewAugmentations(
3985
3669
  await transformPreviewHtml(fallbackHtml, adapter, project, fallback.compositionPath),
3986
3670
  adapter,
@@ -4040,7 +3724,7 @@ ${etag}`;
4040
3724
  function pinSubCompHfIds(compFile, compPath) {
4041
3725
  if (!/\.html?$/i.test(compPath)) return void 0;
4042
3726
  try {
4043
- return ensureHfIds3(readFileSync8(compFile, "utf-8"));
3727
+ return ensureHfIds2(readFileSync6(compFile, "utf-8"));
4044
3728
  } catch {
4045
3729
  return null;
4046
3730
  }
@@ -4059,7 +3743,7 @@ ${etag}`;
4059
3743
  if (!compFile || !existsSync5(compFile) || !statSync4(compFile).isFile()) {
4060
3744
  return c.text("not found", 404);
4061
3745
  }
4062
- const compPathHash = createHash4("sha1").update(compPath).digest("hex");
3746
+ const compPathHash = createHash("sha1").update(compPath).digest("hex");
4063
3747
  const capture = c.req.query(PREVIEW_CAPTURE_PARAM) !== void 0;
4064
3748
  const etag = `"comp:v2:${compPathHash}:${signature}${variablesEtagSalt(vars.raw)}${capture ? ":capture" : ""}"`;
4065
3749
  const ifNoneMatch = c.req.header("If-None-Match");
@@ -4081,7 +3765,7 @@ ${etag}`;
4081
3765
  );
4082
3766
  if (!html) return c.text("not found", 404);
4083
3767
  recordPreviewReferences(project.dir, html);
4084
- html = ensureHfIds3(await transformPreviewHtml(html, adapter, project, compPath));
3768
+ html = ensureHfIds2(await transformPreviewHtml(html, adapter, project, compPath));
4085
3769
  html = injectStudioPreviewAugmentations(html, adapter, project.dir, compPath, capture);
4086
3770
  if (previewVariables) html = injectPreviewVariables(html, previewVariables);
4087
3771
  html = await injectMediaCodecMap(html, adapter, project.dir, compPath, mediaCodecProbeCache);
@@ -4091,14 +3775,14 @@ ${etag}`;
4091
3775
  const project = await adapter.resolveProject(c.req.param("id"));
4092
3776
  if (!project) return c.json({ error: "not found" }, 404);
4093
3777
  const subPath = requestSubPath(c.req.url, "projects/:id/preview");
4094
- const candidate = resolve5(project.dir, subPath);
3778
+ const candidate = resolve4(project.dir, subPath);
4095
3779
  const file = isWithinProjectRoot(project.dir, candidate) ? candidate : null;
4096
3780
  if (!file) {
4097
3781
  return c.text("not found", 404);
4098
3782
  }
4099
3783
  recordPreviewRead(project.dir, file);
4100
- const stat3 = existsSync5(file) ? statSync4(file) : null;
4101
- if (!stat3?.isFile()) {
3784
+ const stat2 = existsSync5(file) ? statSync4(file) : null;
3785
+ if (!stat2?.isFile()) {
4102
3786
  return c.text("not found", 404);
4103
3787
  }
4104
3788
  const contentType = getMimeType(subPath);
@@ -4120,7 +3804,7 @@ ${etag}`;
4120
3804
  return c.text("media proxy variant does not match asset", 422);
4121
3805
  }
4122
3806
  }
4123
- const tag = settledFileTag(stat3);
3807
+ const tag = settledFileTag(stat2);
4124
3808
  const etag = tag && `"${tag}${proxyEtagSalt(proxyVariant)}"`;
4125
3809
  const cacheHeaders = isText ? { "Cache-Control": "no-store" } : { "Cache-Control": "private, no-cache", ...etag && { ETag: etag } };
4126
3810
  if (!isText && etag) {
@@ -4132,10 +3816,15 @@ ${etag}`;
4132
3816
  let servedPath = file;
4133
3817
  let servedContentType = contentType;
4134
3818
  if (proxyVariant !== void 0) {
4135
- recordProxyRequest();
4136
3819
  try {
4137
- servedPath = await resolveProxy(project.dir, file, proxyVariant);
3820
+ servedPath = await waitForProxy(resolveProxy(project.dir, file, proxyVariant), 0);
4138
3821
  } catch (err) {
3822
+ if (err instanceof ProxyWaitTimeoutError) {
3823
+ return c.text("media proxy is being made", 202, {
3824
+ "Retry-After": String(PROXY_PENDING_RETRY_AFTER_SECONDS),
3825
+ "Cache-Control": "no-store"
3826
+ });
3827
+ }
4139
3828
  if (err instanceof ProxyCapacityError) {
4140
3829
  return c.text(err.message, 503, { "Retry-After": "5" });
4141
3830
  }
@@ -4143,9 +3832,10 @@ ${etag}`;
4143
3832
  const message = err instanceof ProxyTranscodeError ? err.message : "proxy transcode failed";
4144
3833
  return c.text(message, 502);
4145
3834
  }
3835
+ recordProxyRequest();
4146
3836
  servedContentType = PROXY_VARIANT_CONFIG[proxyVariant].contentType;
4147
3837
  }
4148
- const textBuffer = isText ? Buffer.from(readFileSync8(file, "utf-8"), "utf-8") : null;
3838
+ const textBuffer = isText ? Buffer.from(readFileSync6(file, "utf-8"), "utf-8") : null;
4149
3839
  const totalSize = textBuffer ? textBuffer.length : statSync4(servedPath).size;
4150
3840
  const bodyFor = (start, end) => textBuffer ? new Uint8Array(textBuffer.subarray(start, end + 1)) : (
4151
3841
  // Node's web stream type and the DOM one do not overlap for tsc on
@@ -4189,8 +3879,8 @@ ${etag}`;
4189
3879
  }
4190
3880
 
4191
3881
  // src/routes/lint.ts
4192
- import { readFileSync as readFileSync9 } from "fs";
4193
- import { join as join10 } from "path";
3882
+ import { readFileSync as readFileSync7 } from "fs";
3883
+ import { join as join9 } from "path";
4194
3884
  async function collectProjectFindings(adapter, projectDir) {
4195
3885
  const findings = [];
4196
3886
  const coveredFiles = /* @__PURE__ */ new Set();
@@ -4208,7 +3898,7 @@ async function lintUncoveredHtml(adapter, projectDir, htmlFiles, coveredFiles) {
4208
3898
  const findings = [];
4209
3899
  for (const file of htmlFiles) {
4210
3900
  if (coveredFiles.has(file)) continue;
4211
- const content = readFileSync9(join10(projectDir, file), "utf-8");
3901
+ const content = readFileSync7(join9(projectDir, file), "utf-8");
4212
3902
  const result = await adapter.lint(content, { filePath: file, isSubComposition: true });
4213
3903
  for (const finding of result?.findings ?? []) {
4214
3904
  findings.push({ ...finding, file });
@@ -4241,8 +3931,8 @@ function registerLintRoutes(api, adapter) {
4241
3931
 
4242
3932
  // src/routes/render.ts
4243
3933
  import { streamSSE } from "hono/streaming";
4244
- import { existsSync as existsSync6, readFileSync as readFileSync10, unlinkSync as unlinkSync3, readdirSync as readdirSync4, statSync as statSync5 } from "fs";
4245
- import { join as join11 } from "path";
3934
+ import { existsSync as existsSync6, readFileSync as readFileSync8, unlinkSync as unlinkSync3, readdirSync as readdirSync3, statSync as statSync5 } from "fs";
3935
+ import { join as join10 } from "path";
4246
3936
  import { VALID_CANVAS_RESOLUTIONS } from "@hyperframes/parsers";
4247
3937
  import { formatRenderOutputTimestamp, parseFps } from "@hyperframes/core";
4248
3938
  var VALID_RESOLUTIONS = new Set(VALID_CANVAS_RESOLUTIONS);
@@ -4311,7 +4001,7 @@ function registerRenderRoutes(api, adapter) {
4311
4001
  const rendersDir = adapter.rendersDir(project);
4312
4002
  mkdirWithinProject(project.dir, rendersDir);
4313
4003
  const ext = FORMAT_EXT[format] ?? ".mp4";
4314
- const outputPath = join11(rendersDir, `${jobId}${ext}`);
4004
+ const outputPath = join10(rendersDir, `${jobId}${ext}`);
4315
4005
  const jobState = adapter.startRender({
4316
4006
  project,
4317
4007
  outputPath,
@@ -4380,7 +4070,7 @@ function registerRenderRoutes(api, adapter) {
4380
4070
  }
4381
4071
  const contentType = renderContentType(job.outputPath);
4382
4072
  const filename = job.outputPath.split("/").pop() ?? `render.mp4`;
4383
- const content = readFileSync10(job.outputPath);
4073
+ const content = readFileSync8(job.outputPath);
4384
4074
  return new Response(content, {
4385
4075
  headers: {
4386
4076
  "Content-Type": contentType,
@@ -4398,7 +4088,7 @@ function registerRenderRoutes(api, adapter) {
4398
4088
  }
4399
4089
  const contentType = renderContentType(job.outputPath);
4400
4090
  const filename = job.outputPath.split("/").pop() ?? `render.mp4`;
4401
- const content = readFileSync10(job.outputPath);
4091
+ const content = readFileSync8(job.outputPath);
4402
4092
  return new Response(content, {
4403
4093
  headers: {
4404
4094
  "Content-Type": contentType,
@@ -4412,7 +4102,7 @@ function registerRenderRoutes(api, adapter) {
4412
4102
  if (state.id === jobId && state.outputPath) {
4413
4103
  const dir = state.outputPath.replace(/\/[^/]+$/, "");
4414
4104
  for (const ext of [".mp4", ".webm", ".mov", ".meta.json"]) {
4415
- const fp = join11(dir, `${jobId}${ext}`);
4105
+ const fp = join10(dir, `${jobId}${ext}`);
4416
4106
  if (existsSync6(fp)) unlinkSync3(fp);
4417
4107
  }
4418
4108
  break;
@@ -4431,7 +4121,7 @@ function registerRenderRoutes(api, adapter) {
4431
4121
  if (!fp) return c.json({ error: "forbidden" }, 403);
4432
4122
  if (!existsSync6(fp)) return c.json({ error: "not found" }, 404);
4433
4123
  const contentType = renderContentType(fp);
4434
- const content = readFileSync10(fp);
4124
+ const content = readFileSync8(fp);
4435
4125
  return new Response(content, {
4436
4126
  headers: {
4437
4127
  "Content-Type": contentType,
@@ -4446,16 +4136,16 @@ function registerRenderRoutes(api, adapter) {
4446
4136
  if (!project) return c.json({ error: "not found" }, 404);
4447
4137
  const rendersDir = adapter.rendersDir(project);
4448
4138
  if (!existsSync6(rendersDir)) return c.json({ renders: [] });
4449
- const files = readdirSync4(rendersDir).filter((f) => f.endsWith(".mp4") || f.endsWith(".webm") || f.endsWith(".mov")).map((f) => {
4450
- const fp = join11(rendersDir, f);
4451
- const stat3 = statSync5(fp);
4139
+ const files = readdirSync3(rendersDir).filter((f) => f.endsWith(".mp4") || f.endsWith(".webm") || f.endsWith(".mov")).map((f) => {
4140
+ const fp = join10(rendersDir, f);
4141
+ const stat2 = statSync5(fp);
4452
4142
  const rid = f.replace(/\.(mp4|webm|mov)$/, "");
4453
- const metaPath = join11(rendersDir, `${rid}.meta.json`);
4143
+ const metaPath = join10(rendersDir, `${rid}.meta.json`);
4454
4144
  let status = "complete";
4455
4145
  let durationMs;
4456
4146
  if (existsSync6(metaPath)) {
4457
4147
  try {
4458
- const meta = JSON.parse(readFileSync10(metaPath, "utf-8"));
4148
+ const meta = JSON.parse(readFileSync8(metaPath, "utf-8"));
4459
4149
  if (meta.status === "failed" && !existsSync6(fp)) status = "failed";
4460
4150
  if (meta.durationMs) durationMs = meta.durationMs;
4461
4151
  } catch {
@@ -4464,8 +4154,8 @@ function registerRenderRoutes(api, adapter) {
4464
4154
  return {
4465
4155
  id: rid,
4466
4156
  filename: f,
4467
- size: stat3.size,
4468
- createdAt: stat3.mtimeMs,
4157
+ size: stat2.size,
4158
+ createdAt: stat2.mtimeMs,
4469
4159
  status,
4470
4160
  durationMs
4471
4161
  };
@@ -4476,7 +4166,7 @@ function registerRenderRoutes(api, adapter) {
4476
4166
  id: file.id,
4477
4167
  status: file.status,
4478
4168
  progress: 100,
4479
- outputPath: join11(rendersDir, file.filename),
4169
+ outputPath: join10(rendersDir, file.filename),
4480
4170
  createdAt: file.createdAt
4481
4171
  });
4482
4172
  }
@@ -4488,7 +4178,7 @@ function registerRenderRoutes(api, adapter) {
4488
4178
  // src/routes/imageThumbnail.ts
4489
4179
  import { HTTPException } from "hono/http-exception";
4490
4180
  import { open, stat } from "fs/promises";
4491
- import { createHash as createHash5 } from "crypto";
4181
+ import { createHash as createHash2 } from "crypto";
4492
4182
  import sharp from "sharp";
4493
4183
 
4494
4184
  // src/routes/thumbnailGenerationCoordinator.ts
@@ -4508,10 +4198,10 @@ var ThumbnailGenerationCoordinator = class {
4508
4198
  if (signal.aborted) return Promise.reject(new DOMException("Aborted", "AbortError"));
4509
4199
  let entry = this.entries.get(key);
4510
4200
  if (!entry) {
4511
- let resolve9;
4201
+ let resolve6;
4512
4202
  let reject;
4513
4203
  const promise = new Promise((resolvePromise, rejectPromise) => {
4514
- resolve9 = resolvePromise;
4204
+ resolve6 = resolvePromise;
4515
4205
  reject = rejectPromise;
4516
4206
  });
4517
4207
  entry = {
@@ -4521,7 +4211,7 @@ var ThumbnailGenerationCoordinator = class {
4521
4211
  state: "queued",
4522
4212
  work,
4523
4213
  promise,
4524
- resolve: resolve9,
4214
+ resolve: resolve6,
4525
4215
  reject
4526
4216
  };
4527
4217
  this.entries.set(key, entry);
@@ -4535,7 +4225,7 @@ var ThumbnailGenerationCoordinator = class {
4535
4225
  return /* @__PURE__ */ new Set([...this.entries.keys(), ...[...this.activeEntries].map((entry) => entry.key)]);
4536
4226
  }
4537
4227
  lease(entry, signal) {
4538
- return new Promise((resolve9, reject) => {
4228
+ return new Promise((resolve6, reject) => {
4539
4229
  let released = false;
4540
4230
  const release = () => {
4541
4231
  if (released) return;
@@ -4559,7 +4249,7 @@ var ThumbnailGenerationCoordinator = class {
4559
4249
  entry.promise.then(
4560
4250
  (value) => {
4561
4251
  release();
4562
- resolve9(value);
4252
+ resolve6(value);
4563
4253
  },
4564
4254
  (reason) => {
4565
4255
  release();
@@ -4624,14 +4314,14 @@ async function generateThumbnail(path, signal) {
4624
4314
  async function resolveSource(adapter, id, url) {
4625
4315
  const project = await adapter.resolveProject(id);
4626
4316
  if (!project) throw new HTTPException(404);
4627
- let relative6;
4317
+ let relative4;
4628
4318
  try {
4629
- relative6 = requestSubPath(url, "projects/:id/image-thumbnail");
4319
+ relative4 = requestSubPath(url, "projects/:id/image-thumbnail");
4630
4320
  } catch {
4631
4321
  throw new HTTPException(400);
4632
4322
  }
4633
- if (!/\.jpe?g$/i.test(relative6)) throw new HTTPException(415);
4634
- const path = pinWithinProject(project.dir, relative6);
4323
+ if (!/\.jpe?g$/i.test(relative4)) throw new HTTPException(415);
4324
+ const path = pinWithinProject(project.dir, relative4);
4635
4325
  if (!path) throw new HTTPException(404);
4636
4326
  const info = await stat(path).catch(() => {
4637
4327
  throw new HTTPException(404);
@@ -4659,7 +4349,7 @@ function registerImageThumbnailRoutes(api, adapter) {
4659
4349
  api.get("/projects/:id/image-thumbnail/*", async (c) => {
4660
4350
  try {
4661
4351
  const { path, info } = await resolveSource(adapter, c.req.param("id"), c.req.url);
4662
- const key = createHash5("sha256").update(JSON.stringify([path, info.dev, info.ino, info.size, info.mtimeMs, info.ctimeMs])).digest("hex");
4352
+ const key = createHash2("sha256").update(JSON.stringify([path, info.dev, info.ino, info.size, info.mtimeMs, info.ctimeMs])).digest("hex");
4663
4353
  const etag = `"${key}"`;
4664
4354
  c.header("ETag", etag);
4665
4355
  c.header("Cache-Control", "private, no-cache");
@@ -4687,22 +4377,22 @@ function registerImageThumbnailRoutes(api, adapter) {
4687
4377
 
4688
4378
  // src/routes/thumbnail.ts
4689
4379
  import {
4690
- closeSync as closeSync3,
4691
- fstatSync as fstatSync3,
4692
- openSync as openSync3,
4693
- readFileSync as readFileSync12,
4694
- readdirSync as readdirSync5,
4380
+ closeSync as closeSync2,
4381
+ fstatSync as fstatSync2,
4382
+ openSync as openSync2,
4383
+ readFileSync as readFileSync10,
4384
+ readdirSync as readdirSync4,
4695
4385
  renameSync as renameSync3,
4696
4386
  rmSync as rmSync4,
4697
4387
  statSync as statSync6,
4698
4388
  unlinkSync as unlinkSync4,
4699
- writeFileSync as writeFileSync6
4389
+ writeFileSync as writeFileSync5
4700
4390
  } from "fs";
4701
- import { join as join12 } from "path";
4702
- import { createHash as createHash6, randomUUID as randomUUID4 } from "crypto";
4391
+ import { join as join11 } from "path";
4392
+ import { createHash as createHash3, randomUUID as randomUUID2 } from "crypto";
4703
4393
 
4704
4394
  // src/helpers/compositionInputs.ts
4705
- import { readFileSync as readFileSync11 } from "fs";
4395
+ import { readFileSync as readFileSync9 } from "fs";
4706
4396
  var ROOT_COMPOSITION = "index.html";
4707
4397
  var COMPOSITION_SRC = /\bdata-composition-src\s*=\s*(["'])(.*?)\1/g;
4708
4398
  function projectReader(projectDir) {
@@ -4712,7 +4402,7 @@ function projectReader(projectDir) {
4712
4402
  const file = resolveWithinProject(projectDir, compPath);
4713
4403
  let source = null;
4714
4404
  try {
4715
- source = file ? readFileSync11(file, "utf-8") : null;
4405
+ source = file ? readFileSync9(file, "utf-8") : null;
4716
4406
  } catch {
4717
4407
  }
4718
4408
  sources.set(compPath, source);
@@ -4768,14 +4458,14 @@ var prunedCacheDirs = /* @__PURE__ */ new Set();
4768
4458
  function pruneThumbnailCache(cacheDir, protectedPaths, now = Date.now()) {
4769
4459
  let entries;
4770
4460
  try {
4771
- entries = readdirSync5(cacheDir, { withFileTypes: true });
4461
+ entries = readdirSync4(cacheDir, { withFileTypes: true });
4772
4462
  } catch (err) {
4773
4463
  if (err.code === "ENOENT") return;
4774
4464
  throw err;
4775
4465
  }
4776
4466
  const files = entries.flatMap((entry) => {
4777
4467
  if (!entry.isFile()) return [];
4778
- const path = join12(cacheDir, entry.name);
4468
+ const path = join11(cacheDir, entry.name);
4779
4469
  try {
4780
4470
  const stats = statSync6(path);
4781
4471
  return [{ path, bytes: stats.size, mtimeMs: stats.mtimeMs }];
@@ -4803,9 +4493,9 @@ function pruneThumbnailCache(cacheDir, protectedPaths, now = Date.now()) {
4803
4493
  }
4804
4494
  }
4805
4495
  function writeThumbnailAtomically(path, buffer) {
4806
- const temporaryPath = `${path}.${process.pid}.${randomUUID4()}.tmp`;
4496
+ const temporaryPath = `${path}.${process.pid}.${randomUUID2()}.tmp`;
4807
4497
  try {
4808
- writeFileSync6(temporaryPath, buffer, { flag: "wx" });
4498
+ writeFileSync5(temporaryPath, buffer, { flag: "wx" });
4809
4499
  renameSync3(temporaryPath, path);
4810
4500
  } finally {
4811
4501
  rmSync4(temporaryPath, { force: true });
@@ -4814,7 +4504,7 @@ function writeThumbnailAtomically(path, buffer) {
4814
4504
  function readFileOnce(file) {
4815
4505
  let fd;
4816
4506
  try {
4817
- fd = openSync3(file, "r");
4507
+ fd = openSync2(file, "r");
4818
4508
  } catch (err) {
4819
4509
  const code = err.code;
4820
4510
  if (code === "ENOENT") return "missing";
@@ -4822,17 +4512,17 @@ function readFileOnce(file) {
4822
4512
  throw err;
4823
4513
  }
4824
4514
  try {
4825
- const stat3 = fstatSync3(fd);
4826
- if (!stat3.isFile()) return "not-a-file";
4827
- return { data: readFileSync12(fd), mtimeMs: stat3.mtimeMs };
4515
+ const stat2 = fstatSync2(fd);
4516
+ if (!stat2.isFile()) return "not-a-file";
4517
+ return { data: readFileSync10(fd), mtimeMs: stat2.mtimeMs };
4828
4518
  } finally {
4829
- closeSync3(fd);
4519
+ closeSync2(fd);
4830
4520
  }
4831
4521
  }
4832
4522
  function manifestKey(file) {
4833
4523
  const manifest = readFileOnce(file);
4834
4524
  if (typeof manifest === "string") return { key: "", mtimeMs: -Infinity };
4835
- const hash = createHash6("sha1").update(manifest.data).digest("hex").slice(0, 16);
4525
+ const hash = createHash3("sha1").update(manifest.data).digest("hex").slice(0, 16);
4836
4526
  return { key: `_${hash}`, mtimeMs: Math.round(manifest.mtimeMs) };
4837
4527
  }
4838
4528
  function registerThumbnailRoutes(api, adapter) {
@@ -4869,7 +4559,7 @@ function registerThumbnailRoutes(api, adapter) {
4869
4559
  let sourceKey = "";
4870
4560
  if (source !== "missing") {
4871
4561
  const html = source.data.toString("utf-8");
4872
- sourceKey = `_${createHash6("sha1").update(html).digest("hex").slice(0, 16)}`;
4562
+ sourceKey = `_${createHash3("sha1").update(html).digest("hex").slice(0, 16)}`;
4873
4563
  sourceMtime = Math.round(source.mtimeMs);
4874
4564
  if (!vpWidth) {
4875
4565
  const wMatch = html.match(/data-width=["'](\d+)["']/);
@@ -4878,21 +4568,21 @@ function registerThumbnailRoutes(api, adapter) {
4878
4568
  if (hMatch?.[1]) compH = parseInt(hMatch[1]);
4879
4569
  }
4880
4570
  }
4881
- const manualEdits = manifestKey(join12(project.dir, STUDIO_MANUAL_EDITS_PATH));
4882
- const motion = manifestKey(join12(project.dir, STUDIO_MOTION_PATH));
4571
+ const manualEdits = manifestKey(join11(project.dir, STUDIO_MANUAL_EDITS_PATH));
4572
+ const motion = manifestKey(join11(project.dir, STUDIO_MOTION_PATH));
4883
4573
  sourceMtime = Math.max(sourceMtime, manualEdits.mtimeMs, motion.mtimeMs);
4884
4574
  const projectUrl = `http://${c.req.header("host")}/api/projects/${encodeURIComponent(project.id)}`;
4885
4575
  const previewPath = compPath === "index.html" ? `${projectUrl}/preview` : `${projectUrl}/preview/comp/${compPath.split("/").map(encodeURIComponent).join("/")}`;
4886
4576
  const previewUrl = `${previewPath}?${PREVIEW_CAPTURE_PARAM}=1`;
4887
- const cacheDir = join12(project.dir, ".thumbnails");
4577
+ const cacheDir = join11(project.dir, ".thumbnails");
4888
4578
  const selectorKey = selector ? `_${selector.replace(/[^a-zA-Z0-9_-]+/g, "_").slice(0, 80)}_${selectorIndex ?? 0}` : "";
4889
4579
  const urlVersionKey = urlVersion ? `_${urlVersion.replace(/[^a-zA-Z0-9_-]+/g, "_").slice(0, 32)}` : "";
4890
- const inputSignatureKey = `_${createHash6("sha1").update(inputSignature).digest("hex").slice(0, 16)}`;
4580
+ const inputSignatureKey = `_${createHash3("sha1").update(inputSignature).digest("hex").slice(0, 16)}`;
4891
4581
  const outputScale = outputMode === "source" ? 1 : Math.min(1, THUMBNAIL_MAX_OUTPUT_WIDTH / compW, THUMBNAIL_MAX_OUTPUT_HEIGHT / compH);
4892
4582
  const outputWidth = Math.max(1, Math.round(compW * outputScale));
4893
4583
  const outputHeight = Math.max(1, Math.round(compH * outputScale));
4894
4584
  const cacheKey = `${THUMBNAIL_CACHE_VERSION}${urlVersionKey}${inputSignatureKey}${manualEdits.key}${motion.key}${sourceKey}_${format}_${outputMode}_${compPath.replace(/\//g, "_")}_${compW}x${compH}_${outputWidth}x${outputHeight}_${sourceMtime}_${seekTime.toFixed(2)}${selectorKey}.${format === "png" ? "png" : "jpg"}`;
4895
- const cachePath = join12(cacheDir, cacheKey);
4585
+ const cachePath = join11(cacheDir, cacheKey);
4896
4586
  if (!prunedCacheDirs.has(cacheDir)) {
4897
4587
  prunedCacheDirs.add(cacheDir);
4898
4588
  pruneThumbnailCache(
@@ -4913,6 +4603,7 @@ function registerThumbnailRoutes(api, adapter) {
4913
4603
  cachePath,
4914
4604
  c.req.raw.signal,
4915
4605
  async (signal) => {
4606
+ const previewCopiesAtStart = proxyActivityMark(project.dir);
4916
4607
  const generated = await adapter.generateThumbnail({
4917
4608
  project,
4918
4609
  compPath,
@@ -4928,9 +4619,10 @@ function registerThumbnailRoutes(api, adapter) {
4928
4619
  signal
4929
4620
  });
4930
4621
  if (!generated) return null;
4622
+ const previewCopiesAtEnd = proxyActivityMark(project.dir);
4931
4623
  const afterGeneration = await resolveProjectAndSignature(adapter, project.id);
4932
4624
  const inputsUnchanged = (signature) => compositionInputSignature(project.dir, compPath, signature) === inputSignature;
4933
- if (afterGeneration?.project.dir !== project.dir || !inputsUnchanged(afterGeneration.signature) || !inputsUnchanged(createProjectSignature(project.dir))) {
4625
+ if (previewCopiesAtStart === null || previewCopiesAtEnd !== previewCopiesAtStart || afterGeneration?.project.dir !== project.dir || !inputsUnchanged(afterGeneration.signature) || !inputsUnchanged(createProjectSignature(project.dir))) {
4934
4626
  return generated;
4935
4627
  }
4936
4628
  mkdirWithinProject(project.dir, cacheDir);
@@ -4960,25 +4652,25 @@ function registerThumbnailRoutes(api, adapter) {
4960
4652
  }
4961
4653
 
4962
4654
  // src/routes/waveform.ts
4963
- import { existsSync as existsSync7, readFileSync as readFileSync13, statSync as statSync7 } from "fs";
4964
- import { join as join13, resolve as resolve6 } from "path";
4655
+ import { existsSync as existsSync7, readFileSync as readFileSync11, statSync as statSync7 } from "fs";
4656
+ import { join as join12, resolve as resolve5 } from "path";
4965
4657
  import { isWithinProjectRoot as isWithinProjectRoot2 } from "@hyperframes/parsers/asset-resolution";
4966
4658
  function registerWaveformRoutes(api, adapter) {
4967
4659
  api.get("/projects/:id/waveform/*", async (c) => {
4968
4660
  const project = await adapter.resolveProject(c.req.param("id"));
4969
4661
  if (!project) return c.json({ error: "not found" }, 404);
4970
4662
  const assetPath = requestSubPath(c.req.url, "projects/:id/waveform");
4971
- const audioPath = resolve6(project.dir, assetPath);
4663
+ const audioPath = resolve5(project.dir, assetPath);
4972
4664
  if (!isWithinProjectRoot2(project.dir, audioPath)) {
4973
4665
  return c.json({ error: "file not found" }, 404);
4974
4666
  }
4975
4667
  const stats = statSync7(audioPath, { throwIfNoEntry: false });
4976
4668
  if (!stats) return c.json({ error: "file not found" }, 404);
4977
- const cacheDir = join13(project.dir, ".waveform-cache");
4978
- const cachePath = join13(cacheDir, buildWaveformCacheKey(assetPath, stats));
4669
+ const cacheDir = join12(project.dir, ".waveform-cache");
4670
+ const cachePath = join12(cacheDir, buildWaveformCacheKey(assetPath, stats));
4979
4671
  try {
4980
4672
  if (isWaveformCacheDirectory(cacheDir) && existsSync7(cachePath)) {
4981
- const peaks2 = JSON.parse(readFileSync13(cachePath, "utf-8"));
4673
+ const peaks2 = JSON.parse(readFileSync11(cachePath, "utf-8"));
4982
4674
  return c.json({ peaks: peaks2 });
4983
4675
  }
4984
4676
  } catch {
@@ -4998,7 +4690,7 @@ function registerWaveformRoutes(api, adapter) {
4998
4690
  }
4999
4691
 
5000
4692
  // src/routes/fonts.ts
5001
- import { closeSync as closeSync4, constants as constants2, fstatSync as fstatSync4, openSync as openSync4, readSync } from "fs";
4693
+ import { closeSync as closeSync3, constants, fstatSync as fstatSync3, openSync as openSync3, readSync } from "fs";
5002
4694
  import {
5003
4695
  collectFontFileEntries,
5004
4696
  fontDirectories,
@@ -5105,17 +4797,17 @@ function registerFontRoutes(api) {
5105
4797
  if (!located) return c.json({ error: "font not found" }, 404);
5106
4798
  let fd;
5107
4799
  try {
5108
- fd = openSync4(located.path, constants2.O_RDONLY | constants2.O_NOFOLLOW);
4800
+ fd = openSync3(located.path, constants.O_RDONLY | constants.O_NOFOLLOW);
5109
4801
  } catch {
5110
4802
  return c.json({ error: "font file not accessible" }, 404);
5111
4803
  }
5112
4804
  try {
5113
- const stat3 = fstatSync4(fd);
5114
- if (stat3.size > SYSTEM_FONT_SIZE_LIMIT) {
4805
+ const stat2 = fstatSync3(fd);
4806
+ if (stat2.size > SYSTEM_FONT_SIZE_LIMIT) {
5115
4807
  return c.json({ error: "font file too large" }, 413);
5116
4808
  }
5117
- const buffer = Buffer.alloc(stat3.size);
5118
- readSync(fd, buffer, 0, stat3.size, 0);
4809
+ const buffer = Buffer.alloc(stat2.size);
4810
+ readSync(fd, buffer, 0, stat2.size, 0);
5119
4811
  const mimeType = located.format === "otf" ? "font/otf" : located.format === "woff2" ? "font/woff2" : located.format === "woff" ? "font/woff" : located.format === "ttc" ? "font/collection" : "font/ttf";
5120
4812
  const fileName = `${family.replace(/[^a-zA-Z0-9 -]/g, "")}.${located.format}`;
5121
4813
  return new Response(buffer, {
@@ -5127,7 +4819,7 @@ function registerFontRoutes(api) {
5127
4819
  } catch {
5128
4820
  return c.json({ error: "failed to read font file" }, 500);
5129
4821
  } finally {
5130
- closeSync4(fd);
4822
+ closeSync3(fd);
5131
4823
  }
5132
4824
  });
5133
4825
  }
@@ -5264,7 +4956,7 @@ function registerSelectionRoutes(api, adapter) {
5264
4956
  // src/routes/media.ts
5265
4957
  import { streamSSE as streamSSE2 } from "hono/streaming";
5266
4958
  import { existsSync as existsSync8 } from "fs";
5267
- import { basename as basename2, dirname as dirname6, extname as extname2, join as join14, posix } from "path";
4959
+ import { basename as basename2, dirname as dirname5, extname, join as join13, posix } from "path";
5268
4960
  var VIDEO_EXTENSIONS = /* @__PURE__ */ new Set([
5269
4961
  ".mp4",
5270
4962
  ".mov",
@@ -5282,10 +4974,10 @@ var VIDEO_OUTPUT_EXTENSIONS = /* @__PURE__ */ new Set([".webm", ".mov"]);
5282
4974
  var QUALITIES = /* @__PURE__ */ new Set(["fast", "balanced", "best"]);
5283
4975
  var DEVICES = /* @__PURE__ */ new Set(["auto", "cpu", "coreml", "cuda"]);
5284
4976
  function isVideoPath(path) {
5285
- return VIDEO_EXTENSIONS.has(extname2(path).toLowerCase());
4977
+ return VIDEO_EXTENSIONS.has(extname(path).toLowerCase());
5286
4978
  }
5287
4979
  function isImagePath(path) {
5288
- return IMAGE_EXTENSIONS.has(extname2(path).toLowerCase());
4980
+ return IMAGE_EXTENSIONS.has(extname(path).toLowerCase());
5289
4981
  }
5290
4982
  function normalizeProjectAssetPath(path) {
5291
4983
  return path.trim().replace(/^[.]\//, "").replace(/[?#].*$/, "");
@@ -5294,14 +4986,14 @@ function containsNullByte(path) {
5294
4986
  return path.includes("\0");
5295
4987
  }
5296
4988
  function slugFileBase(path) {
5297
- const name = basename2(path, extname2(path)).replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
4989
+ const name = basename2(path, extname(path)).replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
5298
4990
  return name || "media";
5299
4991
  }
5300
4992
  function uniqueAssetPath(projectDir, assetPath, taken) {
5301
- const ext = extname2(assetPath);
4993
+ const ext = extname(assetPath);
5302
4994
  const withoutExt = assetPath.slice(0, -ext.length);
5303
4995
  let candidate = assetPath;
5304
- for (let index = 2; taken.has(candidate) || existsSync8(join14(projectDir, candidate)); index++) {
4996
+ for (let index = 2; taken.has(candidate) || existsSync8(join13(projectDir, candidate)); index++) {
5305
4997
  candidate = `${withoutExt}-${index}${ext}`;
5306
4998
  }
5307
4999
  return candidate;
@@ -5401,10 +5093,10 @@ function registerMediaRoutes(api, adapter, options = {}) {
5401
5093
  const outputAssetPath = requestedOutput ? uniqueAssetPath(project.dir, requestedOutput, taken) : defaultOutputPath(project.dir, inputAssetPath, taken);
5402
5094
  const outputPath = pinWithinProject(project.dir, outputAssetPath);
5403
5095
  if (!outputPath) return c.json({ error: "forbidden" }, 403);
5404
- if (inputIsVideo && !VIDEO_OUTPUT_EXTENSIONS.has(extname2(outputAssetPath).toLowerCase())) {
5096
+ if (inputIsVideo && !VIDEO_OUTPUT_EXTENSIONS.has(extname(outputAssetPath).toLowerCase())) {
5405
5097
  return c.json({ error: "video background removal output must be .webm or .mov" }, 400);
5406
5098
  }
5407
- if (inputIsImage && extname2(outputAssetPath).toLowerCase() !== ".png") {
5099
+ if (inputIsImage && extname(outputAssetPath).toLowerCase() !== ".png") {
5408
5100
  return c.json({ error: "image background removal output must be .png" }, 400);
5409
5101
  }
5410
5102
  let backgroundOutputAssetPath;
@@ -5420,8 +5112,8 @@ function registerMediaRoutes(api, adapter, options = {}) {
5420
5112
  return c.json({ error: "forbidden" }, 403);
5421
5113
  }
5422
5114
  }
5423
- mkdirWithinProject(project.dir, dirname6(outputPath));
5424
- if (backgroundOutputPath) mkdirWithinProject(project.dir, dirname6(backgroundOutputPath));
5115
+ mkdirWithinProject(project.dir, dirname5(outputPath));
5116
+ if (backgroundOutputPath) mkdirWithinProject(project.dir, dirname5(backgroundOutputPath));
5425
5117
  const jobId = makeJobId(project.id, mediaJobs);
5426
5118
  const state = adapter.startBackgroundRemoval({
5427
5119
  project,
@@ -5478,14 +5170,14 @@ function registerMediaRoutes(api, adapter, options = {}) {
5478
5170
  }
5479
5171
 
5480
5172
  // src/routes/globalAssets.ts
5481
- import { existsSync as existsSync9, readFileSync as readFileSync14 } from "fs";
5173
+ import { existsSync as existsSync9, readFileSync as readFileSync12 } from "fs";
5482
5174
  import { homedir } from "os";
5483
- import { join as join15 } from "path";
5175
+ import { join as join14 } from "path";
5484
5176
  function readGlobalAssets(home = homedir()) {
5485
- const manifestPath = join15(home, ".media", "manifest.jsonl");
5177
+ const manifestPath = join14(home, ".media", "manifest.jsonl");
5486
5178
  if (!existsSync9(manifestPath)) return [];
5487
5179
  const out = [];
5488
- for (const line of readFileSync14(manifestPath, "utf8").split("\n")) {
5180
+ for (const line of readFileSync12(manifestPath, "utf8").split("\n")) {
5489
5181
  if (!line.trim()) continue;
5490
5182
  try {
5491
5183
  const rec = JSON.parse(line);
@@ -5502,1269 +5194,6 @@ function registerGlobalAssetRoutes(api) {
5502
5194
  api.get("/assets/global", (c) => c.json({ assets: readGlobalAssets().map(toPublicAsset) }));
5503
5195
  }
5504
5196
 
5505
- // src/history/projectHistory.ts
5506
- import { randomUUID as randomUUID9 } from "crypto";
5507
- import { readdir as readdir3, rm as rm3, rmdir, writeFile } from "fs/promises";
5508
- import {
5509
- existsSync as existsSync11,
5510
- lstatSync as lstatSync4,
5511
- statSync as statSync10,
5512
- mkdirSync as mkdirSync5,
5513
- readdirSync as readdirSync7,
5514
- readFileSync as readFileSync19,
5515
- realpathSync,
5516
- rmSync as rmSync6
5517
- } from "fs";
5518
- import { homedir as homedir2 } from "os";
5519
- import { dirname as dirname10, isAbsolute as isAbsolute2, join as join20, relative as relative5, resolve as resolve8, sep as sep3 } from "path";
5520
-
5521
- // src/history/blobStore.ts
5522
- import { createHash as createHash7, randomUUID as randomUUID5 } from "crypto";
5523
- import { constants as constants3, createReadStream as createReadStream2, renameSync as renameSync4 } from "fs";
5524
- import { copyFile, mkdir, readdir, readFile as readFile2, rename, rm, stat as stat2 } from "fs/promises";
5525
- import { dirname as dirname7, join as join16 } from "path";
5526
- var BLOB_HASH = /^[0-9a-f]{64}$/;
5527
- async function hashFile(path) {
5528
- const hash = createHash7("sha256");
5529
- for await (const chunk of createReadStream2(path)) hash.update(chunk);
5530
- return hash.digest("hex");
5531
- }
5532
- async function cloneOrCopy(from, to, beforeReplace) {
5533
- await mkdir(dirname7(to), { recursive: true });
5534
- const temp = `${to}.${randomUUID5()}.tmp`;
5535
- try {
5536
- await copyFile(from, temp, constants3.COPYFILE_FICLONE);
5537
- beforeReplace?.();
5538
- renameSync4(temp, to);
5539
- } catch (error) {
5540
- await rm(temp, { force: true });
5541
- throw error;
5542
- }
5543
- }
5544
- async function openBlobStore(dir) {
5545
- const sizes = /* @__PURE__ */ new Map();
5546
- await mkdir(dir, { recursive: true });
5547
- for (const shard of await readdir(dir))
5548
- for (const hash of await readdir(join16(dir, shard)).catch(() => []))
5549
- sizes.set(hash, (await stat2(join16(dir, shard, hash))).size);
5550
- const pathOf = (hash) => {
5551
- if (!BLOB_HASH.test(hash)) throw new Error("That is not a history blob.");
5552
- return join16(dir, hash.slice(0, 2), hash);
5553
- };
5554
- let total = [...sizes.values()].reduce((sum, size) => sum + size, 0);
5555
- return {
5556
- async put(absPath) {
5557
- const temp = join16(dir, `incoming-${randomUUID5()}`);
5558
- await mkdir(dir, { recursive: true });
5559
- await copyFile(absPath, temp, constants3.COPYFILE_FICLONE);
5560
- const hash = await hashFile(temp);
5561
- if (sizes.has(hash)) {
5562
- await rm(temp, { force: true });
5563
- return hash;
5564
- }
5565
- await mkdir(dirname7(pathOf(hash)), { recursive: true });
5566
- const size = (await stat2(temp)).size;
5567
- await rename(temp, pathOf(hash));
5568
- sizes.set(hash, size);
5569
- total += size;
5570
- return hash;
5571
- },
5572
- has: (hash) => sizes.has(hash),
5573
- read: async (hash) => readFile2(pathOf(hash)),
5574
- writeTo: async (hash, absPath, beforeReplace) => cloneOrCopy(pathOf(hash), absPath, beforeReplace),
5575
- bytes: () => total,
5576
- size: (hash) => sizes.get(hash) ?? 0,
5577
- async prune(keep) {
5578
- for (const [hash, size] of sizes) {
5579
- if (keep.has(hash)) continue;
5580
- await rm(pathOf(hash), { force: true });
5581
- sizes.delete(hash);
5582
- total -= size;
5583
- }
5584
- }
5585
- };
5586
- }
5587
-
5588
- // src/history/pruneHistories.ts
5589
- import { randomUUID as randomUUID8 } from "crypto";
5590
- import { readdirSync as readdirSync6, readFileSync as readFileSync17, renameSync as renameSync5, statSync as statSync9, writeFileSync as writeFileSync9 } from "fs";
5591
- import { lstat, readdir as readdir2, rm as rm2 } from "fs/promises";
5592
- import { tmpdir as tmpdir2 } from "os";
5593
- import { dirname as dirname8, join as join19 } from "path";
5594
-
5595
- // src/history/historyId.ts
5596
- import { randomUUID as randomUUID6 } from "crypto";
5597
- import { existsSync as existsSync10, mkdirSync as mkdirSync2, readFileSync as readFileSync15, statSync as statSync8, writeFileSync as writeFileSync7 } from "fs";
5598
- import { basename as basename3, join as join17, resolve as resolve7 } from "path";
5599
- var ID_PATH = join17(".hyperframes", "history-id");
5600
- var ID_SHAPE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
5601
- var isHistoryId = (name) => ID_SHAPE.test(name);
5602
- function readId(projectDir) {
5603
- try {
5604
- const id = readFileSync15(join17(projectDir, ID_PATH), "utf-8").trim();
5605
- return ID_SHAPE.test(id) ? id : null;
5606
- } catch {
5607
- return null;
5608
- }
5609
- }
5610
- var sameFolder = (a, b) => a.ino === b.ino && a.birthtimeMs === b.birthtimeMs;
5611
- function isRecordedFolder(historyDir, folder) {
5612
- const was = readRecord(historyDir);
5613
- if (typeof was?.ino === "number")
5614
- return sameFolder({ ino: was.ino, birthtimeMs: was.born ?? NaN }, folder);
5615
- const unreadableSo0878KeptTheId = typeof was?.dir !== "string";
5616
- if (unreadableSo0878KeptTheId) return true;
5617
- return !isCopyOf0878Folder(was.dir, basename3(historyDir), folder);
5618
- }
5619
- function readRecord(historyDir) {
5620
- try {
5621
- return JSON.parse(readFileSync15(join17(historyDir, "project.json"), "utf-8"));
5622
- } catch {
5623
- return null;
5624
- }
5625
- }
5626
- function isCopyOf0878Folder(recordedDir, id, folder) {
5627
- let there;
5628
- try {
5629
- there = statSync8(recordedDir, { throwIfNoEntry: false });
5630
- } catch {
5631
- return false;
5632
- }
5633
- return !!there && !sameFolder(there, folder) && readId(recordedDir) === id;
5634
- }
5635
- function projectHistoryId(projectDir, historyRoot) {
5636
- const dir = resolve7(projectDir);
5637
- const folder = statSync8(dir);
5638
- let id = readId(dir);
5639
- if (!id && existsSync10(join17(dir, ID_PATH)))
5640
- throw new HistoryIdError(
5641
- `${join17(dir, ID_PATH)} holds no history id this version can read; move it aside to start anew.`
5642
- );
5643
- if (!id || existsSync10(join17(historyRoot, id)) && !isRecordedFolder(join17(historyRoot, id), folder)) {
5644
- id = randomUUID6();
5645
- mkdirWithinProject(dir, join17(dir, ".hyperframes"));
5646
- writeFileSync7(join17(dir, ID_PATH), `${id}
5647
- `);
5648
- }
5649
- recordProject(join17(historyRoot, id), dir, folder);
5650
- return id;
5651
- }
5652
- var HistoryIdError = class extends Error {
5653
- constructor(message) {
5654
- super(message);
5655
- this.name = "HistoryIdError";
5656
- }
5657
- };
5658
- function recordProject(historyDir, dir, folder) {
5659
- const record = { dir, ino: folder.ino, born: folder.birthtimeMs, dev: folder.dev };
5660
- mkdirSync2(historyDir, { recursive: true });
5661
- replaceFileAtomically(join17(historyDir, "project.json"), JSON.stringify(record), 420);
5662
- }
5663
-
5664
- // src/history/ownerLock.ts
5665
- import { randomUUID as randomUUID7 } from "crypto";
5666
- import { linkSync, mkdirSync as mkdirSync3, readFileSync as readFileSync16, rmSync as rmSync5, writeFileSync as writeFileSync8 } from "fs";
5667
- import { join as join18 } from "path";
5668
- var HistoryBusyError = class extends Error {
5669
- constructor(pid) {
5670
- super(`This project's history is open in another process (pid ${pid}).`);
5671
- this.pid = pid;
5672
- this.name = "HistoryBusyError";
5673
- }
5674
- pid;
5675
- };
5676
- function alive(pid) {
5677
- if (!Number.isInteger(pid) || pid <= 0) return false;
5678
- try {
5679
- process.kill(pid, 0);
5680
- return true;
5681
- } catch (error) {
5682
- return error.code === "EPERM";
5683
- }
5684
- }
5685
- function ownerOf(file) {
5686
- try {
5687
- const text2 = readFileSync16(file, "utf-8");
5688
- return /^\d+$/.test(text2) ? Number(text2) : Number.NaN;
5689
- } catch (error) {
5690
- if (error.code === "ENOENT") return null;
5691
- throw error;
5692
- }
5693
- }
5694
- function claim(file) {
5695
- const draft = `${file}-${randomUUID7()}.tmp`;
5696
- writeFileSync8(draft, String(process.pid));
5697
- try {
5698
- linkSync(draft, file);
5699
- return true;
5700
- } catch (error) {
5701
- if (error.code !== "EEXIST") throw error;
5702
- return false;
5703
- } finally {
5704
- rmSync5(draft, { force: true });
5705
- }
5706
- }
5707
- function releaseOwn(file) {
5708
- if (ownerOf(file) === process.pid) rmSync5(file, { force: true });
5709
- }
5710
- function evictDeadOwner(file) {
5711
- const evictor = `${file}.evict`;
5712
- if (!claim(evictor)) {
5713
- const holder = ownerOf(evictor);
5714
- if (holder !== null && !alive(holder)) rmSync5(evictor, { force: true });
5715
- return false;
5716
- }
5717
- try {
5718
- const pid = ownerOf(file);
5719
- if (pid !== null && !alive(pid)) rmSync5(file, { force: true });
5720
- return true;
5721
- } finally {
5722
- releaseOwn(evictor);
5723
- }
5724
- }
5725
- async function takeHistoryOwnership(home, waitMs) {
5726
- const file = join18(home, "owner.pid");
5727
- const deadline = Date.now() + waitMs;
5728
- mkdirSync3(home, { recursive: true });
5729
- for (; ; ) {
5730
- if (claim(file)) {
5731
- let held = true;
5732
- return () => {
5733
- if (held) releaseOwn(file);
5734
- held = false;
5735
- };
5736
- }
5737
- const pid = ownerOf(file);
5738
- if (pid === null) continue;
5739
- if (!alive(pid) && evictDeadOwner(file)) continue;
5740
- if (Date.now() >= deadline) throw new HistoryBusyError(pid);
5741
- await new Promise((settle) => setTimeout(settle, 50));
5742
- }
5743
- }
5744
-
5745
- // src/history/pruneHistories.ts
5746
- var DAY_MS = 24 * 60 * 60 * 1e3;
5747
- var KEEP_GONE_PROJECT_HISTORY_MS = 14 * DAY_MS;
5748
- var PRUNED_PREFIX = ".pruned-";
5749
- var LAST_PRUNE_FILE = ".last-prune";
5750
- function listProjectHistories(historyRoot) {
5751
- return namesIn(historyRoot).filter(isHistoryId).flatMap((id) => {
5752
- const home = join19(historyRoot, id);
5753
- const projectDir = readRecord(home)?.dir;
5754
- if (typeof projectDir !== "string" || readId(projectDir) !== id) return [];
5755
- return [{ id, projectDir, lastUsedMs: lastUsed(home) }];
5756
- });
5757
- }
5758
- function namesIn(historyRoot) {
5759
- try {
5760
- return readdirSync6(historyRoot);
5761
- } catch (error) {
5762
- if (error.code === "ENOENT") return [];
5763
- throw error;
5764
- }
5765
- }
5766
- async function pruneGoneProjectHistories(historyRoot, options = {}) {
5767
- return (await pruneWithin(historyRoot, options)).pruned;
5768
- }
5769
- async function pruneWithin(historyRoot, {
5770
- now = Date.now(),
5771
- dryRun = false,
5772
- tempDir = tmpdir2(),
5773
- onError = (error) => console.warn("[history] could not prune a history:", error),
5774
- budgetMs = Infinity,
5775
- measure = true
5776
- }) {
5777
- const started = performance.now();
5778
- const names = namesIn(historyRoot);
5779
- const pruned = [];
5780
- for (const name of names) {
5781
- if (performance.now() - started >= budgetMs) return { pruned, finished: false };
5782
- try {
5783
- const gone = await pruneEntry(historyRoot, name, { now, dryRun, tempDir, measure });
5784
- if (gone) pruned.push(gone);
5785
- } catch (error) {
5786
- onError(error);
5787
- }
5788
- }
5789
- return { pruned, finished: true };
5790
- }
5791
- async function pruneEntry(historyRoot, name, run) {
5792
- if (name.startsWith(PRUNED_PREFIX)) {
5793
- if (!run.dryRun) await rm2(join19(historyRoot, name), { recursive: true, force: true });
5794
- return null;
5795
- }
5796
- return isHistoryId(name) ? pruneOne(historyRoot, name, run) : null;
5797
- }
5798
- async function pruneOne(historyRoot, id, run) {
5799
- const home = join19(historyRoot, id);
5800
- if (abandonedProject(home, id, run.now, run.tempDir) === null) return null;
5801
- const release = await takeUnlessBusy(home);
5802
- if (!release) return null;
5803
- let taken;
5804
- try {
5805
- taken = recheckAndMoveAway(historyRoot, id, run);
5806
- } catch (error) {
5807
- release();
5808
- throw error;
5809
- }
5810
- if (!taken?.trash) release();
5811
- if (!taken) return null;
5812
- const bytes = await clearAway(taken.trash, home, run.measure);
5813
- return { id, projectDir: taken.projectDir, bytes };
5814
- }
5815
- async function clearAway(trash, home, measure) {
5816
- const bytes = measure ? await bytesIn(trash ?? home) : 0;
5817
- if (trash) await rm2(trash, { recursive: true, force: true });
5818
- return bytes;
5819
- }
5820
- async function takeUnlessBusy(home) {
5821
- try {
5822
- return await takeHistoryOwnership(home, 0);
5823
- } catch (error) {
5824
- if (error instanceof HistoryBusyError) return null;
5825
- throw error;
5826
- }
5827
- }
5828
- function recheckAndMoveAway(historyRoot, id, { now, dryRun, tempDir }) {
5829
- const home = join19(historyRoot, id);
5830
- const projectDir = abandonedProject(home, id, now, tempDir);
5831
- if (projectDir === null) return null;
5832
- if (dryRun) return { projectDir, trash: null };
5833
- const trash = join19(historyRoot, `${PRUNED_PREFIX}${id}-${randomUUID8()}`);
5834
- renameSync5(home, trash);
5835
- return { projectDir, trash };
5836
- }
5837
- function abandonedProject(home, id, now, tempDir) {
5838
- const record = readRecord(home);
5839
- const dir = record?.dir;
5840
- if (typeof dir !== "string") return null;
5841
- if (now - lastUsed(home) < KEEP_GONE_PROJECT_HISTORY_MS && !isSafePath(tempDir, dir)) return null;
5842
- return projectGone(dir, id, record?.dev, tempDir) ? dir : null;
5843
- }
5844
- function projectGone(dir, id, dev, tempDir) {
5845
- if (!statSync9(join19(dir, ID_PATH), { throwIfNoEntry: false }))
5846
- return diskStillHere(dir, dev, tempDir);
5847
- const there = readId(dir);
5848
- return there !== null && there !== id;
5849
- }
5850
- function diskStillHere(dir, dev, tempDir) {
5851
- let at = dir;
5852
- let stat3 = statSync9(at, { throwIfNoEntry: false });
5853
- while (!stat3) {
5854
- if (dirname8(at) === at) return false;
5855
- at = dirname8(at);
5856
- stat3 = statSync9(at, { throwIfNoEntry: false });
5857
- }
5858
- if (typeof dev === "number") return stat3.dev === dev;
5859
- return at === dir || isSafePath(tempDir, at);
5860
- }
5861
- function lastUsed(home) {
5862
- const mtime = (file) => statSync9(join19(home, file), { throwIfNoEntry: false })?.mtimeMs ?? 0;
5863
- return Math.max(mtime("project.json"), mtime("log.jsonl"));
5864
- }
5865
- async function bytesIn(dir) {
5866
- let total = 0;
5867
- for (const entry of await readdir2(dir, { withFileTypes: true })) {
5868
- const path = join19(dir, entry.name);
5869
- total += entry.isDirectory() ? await bytesIn(path) : (await lstat(path)).size;
5870
- }
5871
- return total;
5872
- }
5873
- function prunedWithinADay(stamp, now) {
5874
- try {
5875
- const last = Number(readFileSync17(stamp, "utf-8"));
5876
- return last <= now && now - last < DAY_MS;
5877
- } catch {
5878
- return false;
5879
- }
5880
- }
5881
- var pruning = /* @__PURE__ */ new Set();
5882
- function pruneGoneProjectHistoriesDaily(historyRoot, now, { onError, budgetMs } = {}) {
5883
- const stamp = join19(historyRoot, LAST_PRUNE_FILE);
5884
- if (pruning.has(historyRoot) || prunedWithinADay(stamp, now)) return;
5885
- pruning.add(historyRoot);
5886
- setImmediate(() => {
5887
- pruneWithin(historyRoot, { now, onError, budgetMs, measure: false }).then(({ finished }) => finished && writeFileSync9(stamp, String(now))).catch((error) => onError?.(error)).finally(() => pruning.delete(historyRoot));
5888
- });
5889
- }
5890
-
5891
- // src/history/historyLog.ts
5892
- import { closeSync as closeSync5, constants as constants4, mkdirSync as mkdirSync4, openSync as openSync5, readFileSync as readFileSync18, writeSync as writeSync2 } from "fs";
5893
- import { dirname as dirname9 } from "path";
5894
- var START = "start";
5895
- function parseRecord(line) {
5896
- try {
5897
- return JSON.parse(line);
5898
- } catch {
5899
- return null;
5900
- }
5901
- }
5902
- function readLog(file, onUnreadable) {
5903
- let text2;
5904
- try {
5905
- text2 = readFileSync18(file, "utf-8");
5906
- } catch {
5907
- return null;
5908
- }
5909
- const log = { baseline: /* @__PURE__ */ new Map(), entries: [], pins: /* @__PURE__ */ new Set() };
5910
- const lines = text2.split("\n");
5911
- lines.forEach((line, index) => {
5912
- const record = line ? parseRecord(line) : null;
5913
- if (record) applyRecord(log, record);
5914
- else if (line && index < lines.length - 1) onUnreadable(index + 1);
5915
- });
5916
- return log;
5917
- }
5918
- function applyRecord(log, record) {
5919
- if (record.type === "baseline") log.baseline = new Map(Object.entries(record.files));
5920
- else if (record.type === "entry") log.entries.push(record.entry);
5921
- else if (record.pinned) log.pins.add(record.id);
5922
- else log.pins.delete(record.id);
5923
- }
5924
- function saveRecord(file, log, record) {
5925
- let fd;
5926
- try {
5927
- fd = openSync5(file, constants4.O_WRONLY | constants4.O_APPEND);
5928
- } catch (error) {
5929
- if (error.code !== "ENOENT") throw error;
5930
- return writeLog(file, log);
5931
- }
5932
- try {
5933
- writeSync2(fd, `${JSON.stringify(record)}
5934
- `);
5935
- } finally {
5936
- closeSync5(fd);
5937
- }
5938
- }
5939
- function writeLog(file, log) {
5940
- const records = [
5941
- { type: "baseline", files: Object.fromEntries(log.baseline) },
5942
- ...log.entries.map((entry) => ({ type: "entry", entry })),
5943
- ...[...log.pins].map((id) => ({ type: "pin", id, pinned: true }))
5944
- ];
5945
- mkdirSync4(dirname9(file), { recursive: true });
5946
- replaceFileAtomically(file, records.map((r) => `${JSON.stringify(r)}
5947
- `).join(""), 420);
5948
- }
5949
- function applyEntry(manifest, entry, side) {
5950
- for (const file of entry.files) {
5951
- const hash = file[side];
5952
- if (hash === null) manifest.delete(file.path);
5953
- else manifest.set(file.path, hash);
5954
- }
5955
- }
5956
- function manifestAt(log, point) {
5957
- return point === START ? new Map(log.baseline) : manifestAround(log, point, "after");
5958
- }
5959
- function manifestAround(log, id, side) {
5960
- const manifest = new Map(log.baseline);
5961
- for (const entry of log.entries) {
5962
- applyEntry(manifest, entry, entry.id === id ? side : "after");
5963
- if (entry.id === id) return manifest;
5964
- }
5965
- return null;
5966
- }
5967
- function undoneIds(entries) {
5968
- const undone = /* @__PURE__ */ new Set();
5969
- for (let i = entries.length - 1; i >= 0; i -= 1) {
5970
- const entry = entries[i];
5971
- if (entry.undoes && !undone.has(entry.id)) undone.add(entry.undoes);
5972
- }
5973
- return undone;
5974
- }
5975
- function stepTarget(entries, direction, mine) {
5976
- const undone = undoneIds(entries);
5977
- const byId = new Map(entries.map((entry) => [entry.id, entry]));
5978
- const owns = ownerOf2(entries, byId, mine);
5979
- const newestFirst = [...entries].reverse();
5980
- if (direction === "back")
5981
- return newestFirst.find((entry) => !entry.undoes && !undone.has(entry.id) && owns(entry));
5982
- for (const entry of newestFirst) {
5983
- if (!entry.undoes) {
5984
- if (owns(entry)) return void 0;
5985
- continue;
5986
- }
5987
- const target = byId.get(entry.undoes);
5988
- if (mine(entry) && !undone.has(entry.id) && target && !target.undoes) return entry;
5989
- }
5990
- return void 0;
5991
- }
5992
- function ownerOf2(entries, byId, mine) {
5993
- const redoneBy = /* @__PURE__ */ new Map();
5994
- for (const entry of entries) {
5995
- const target = entry.undoes ? byId.get(entry.undoes) : void 0;
5996
- if (target?.undoes) redoneBy.set(target.undoes, entry);
5997
- }
5998
- return (entry) => mine(entry) || mine(redoneBy.get(entry.id) ?? entry);
5999
- }
6000
- function foldOldest(log) {
6001
- const oldest = log.entries[0];
6002
- if (!oldest || log.pins.has(oldest.id)) return false;
6003
- applyEntry(log.baseline, oldest, "after");
6004
- log.entries.shift();
6005
- return true;
6006
- }
6007
- function referencedHashes(log, current) {
6008
- const keep = /* @__PURE__ */ new Set([...log.baseline.values(), ...current.values()]);
6009
- for (const entry of log.entries)
6010
- for (const file of entry.files) {
6011
- if (file.before) keep.add(file.before);
6012
- if (file.after) keep.add(file.after);
6013
- }
6014
- return keep;
6015
- }
6016
-
6017
- // src/history/projectHistory.ts
6018
- var DEFAULT_HISTORY_ROOT = join20(homedir2(), ".cache", "hyperframes", "history");
6019
- var MAX_WINDOW_IDLE_MS = 10 * 6e4;
6020
- var UNDO_MODES = ["just-this", "back-to-before", "keep-later-edits"];
6021
- var OUTSIDE = { kind: "outside", name: "Outside" };
6022
- var OUTSIDE_LABEL = "Changed outside the app";
6023
- var RACY_MS = 2e3;
6024
- var statKey = (file, sweptAt) => sweptAt - Math.max(file.mtimeMs, file.ctimeMs) < RACY_MS ? "" : `${file.size}:${file.mtimeMs}:${file.ctimeMs}`;
6025
- var sameWho = (a, b) => a.kind === b.kind && a.name === b.name;
6026
- function splitAt(change, at, kept) {
6027
- if (at === change.after) return [change, null];
6028
- if (!at || at === change.before || !kept) return [null, change];
6029
- return [
6030
- { ...change, after: at },
6031
- { ...change, before: at }
6032
- ];
6033
- }
6034
- var isMissingFolder = (error) => ["ENOENT", "ENOTDIR"].includes(error.code ?? "");
6035
- async function removeEmptyFolders(dir) {
6036
- let entries;
6037
- try {
6038
- entries = await readdir3(dir, { withFileTypes: true });
6039
- } catch (error) {
6040
- if (isMissingFolder(error)) return;
6041
- throw error;
6042
- }
6043
- for (const entry of entries)
6044
- if (entry.isDirectory()) await removeEmptyFolders(join20(dir, entry.name));
6045
- await rmdir(dir);
6046
- }
6047
- function fileLeftIn(dir, at, deleted) {
6048
- for (const entry of readdirSync7(dir, { withFileTypes: true })) {
6049
- const path = `${at}/${entry.name}`;
6050
- const left = entry.isDirectory() ? fileLeftIn(join20(dir, entry.name), path, deleted) : deleted.has(path) ? void 0 : path;
6051
- if (left) return left;
6052
- }
6053
- return void 0;
6054
- }
6055
- function ignoresCase(dir) {
6056
- const self = statSync10(join20(dir, ID_PATH), { throwIfNoEntry: false });
6057
- const other = statSync10(join20(dir, ID_PATH.toUpperCase()), { throwIfNoEntry: false });
6058
- return self !== void 0 && other?.ino === self.ino && other.dev === self.dev;
6059
- }
6060
- var blocks = (removed, added) => added.startsWith(`${removed}/`) || removed.startsWith(`${added}/`);
6061
- var takesWrite = (window, at) => window.idleMs === void 0 || at - (window.lastWriteAt ?? at) <= window.idleMs;
6062
- function addChange(group, path, before, after) {
6063
- const earlier = group.changes.get(path);
6064
- const from = earlier ? earlier.before : before;
6065
- if (from === after) group.changes.delete(path);
6066
- else group.changes.set(path, { path, before: from, after });
6067
- }
6068
- var Engine = class {
6069
- constructor(options, projectId, blobs) {
6070
- this.options = options;
6071
- this.projectId = projectId;
6072
- this.blobs = blobs;
6073
- this.dir = resolve8(options.projectDir);
6074
- this.home = join20(options.historyRoot, projectId);
6075
- this.foldsCase = ignoresCase(this.dir);
6076
- const folder = statSync10(this.dir, { throwIfNoEntry: false });
6077
- if (!folder || readId(this.dir) !== projectId || !isRecordedFolder(this.home, folder))
6078
- throw this.replaced();
6079
- this.folder = { dev: folder.dev, ino: folder.ino, birthtimeMs: folder.birthtimeMs };
6080
- }
6081
- options;
6082
- projectId;
6083
- blobs;
6084
- dir;
6085
- foldsCase;
6086
- folder;
6087
- home;
6088
- log = { baseline: /* @__PURE__ */ new Map(), entries: [], pins: /* @__PURE__ */ new Set() };
6089
- tracked = /* @__PURE__ */ new Map();
6090
- windows = [];
6091
- outside = null;
6092
- /** A coalescing claim, open until another key, its idle timer, an operation, a window, or another write. */
6093
- claimed = null;
6094
- writeToken;
6095
- quietTimer;
6096
- maxTimer;
6097
- notedTimer = null;
6098
- listeners = /* @__PURE__ */ new Set();
6099
- tail = Promise.resolve();
6100
- closed = false;
6101
- closing;
6102
- /** A path as the project's disk compares names: folded where `A` and `a` are one name. */
6103
- nameKey(path) {
6104
- return this.foldsCase ? path.toLowerCase() : path;
6105
- }
6106
- get logFile() {
6107
- return join20(this.home, "log.jsonl");
6108
- }
6109
- now() {
6110
- return (this.options.now ?? Date.now)();
6111
- }
6112
- /** One operation at a time, in order: sweeps, windows and undos never interleave. */
6113
- queue(task) {
6114
- if (this.closed)
6115
- return Promise.reject(new HistoryClosedError("This project's history is closed."));
6116
- const run = this.tail.then(task);
6117
- this.tail = run.catch(() => void 0);
6118
- return run;
6119
- }
6120
- /** Another project if the folder or its id changed, or the id is gone: a recreated folder may reuse the inode. */
6121
- whereFolder() {
6122
- const now = statSync10(this.dir, { throwIfNoEntry: false });
6123
- if (!now) return "gone";
6124
- const same = now.dev === this.folder.dev && sameFolder(now, this.folder);
6125
- return same && readId(this.dir) === this.projectId ? "here" : "replaced";
6126
- }
6127
- assertWritable() {
6128
- const folder = this.whereFolder();
6129
- if (folder === "gone") throw new Error(`The project folder is gone: ${this.dir}`);
6130
- if (folder === "replaced") throw this.replaced();
6131
- }
6132
- persistLog(record) {
6133
- if (record) saveRecord(this.logFile, this.log, record);
6134
- else writeLog(this.logFile, this.log);
6135
- if (!existsSync11(join20(this.home, "project.json")))
6136
- recordProject(this.home, this.dir, this.folder);
6137
- }
6138
- replaced() {
6139
- return new HistoryClosedError(`${this.dir} is now another project.`);
6140
- }
6141
- assertOpen() {
6142
- if (this.closed) throw new HistoryClosedError("This project's history is closed.");
6143
- if (this.whereFolder() === "replaced") throw this.replaced();
6144
- }
6145
- async start() {
6146
- const log = readLog(
6147
- this.logFile,
6148
- (line) => this.options.onError?.(
6149
- new Error(`History log line ${line} could not be read and was skipped.`)
6150
- )
6151
- );
6152
- if (!log) return this.firstOpen();
6153
- this.log = log;
6154
- const cache = this.readStatCache();
6155
- const last = this.log.entries.at(-1)?.id ?? START;
6156
- for (const [path, hash] of manifestAt(this.log, last) ?? []) {
6157
- const cached = cache.get(path);
6158
- this.tracked.set(path, { hash, stat: cached?.hash === hash ? cached.stat : "" });
6159
- }
6160
- this.reopenClosedWindow();
6161
- await this.settleAll();
6162
- }
6163
- reopenClosedWindow() {
6164
- const closed = this.options.closedWindow;
6165
- if (!closed || this.log.entries.some((entry) => entry.id === closed.id)) return;
6166
- const { id, who, label, startedAt, lastWriteAt, idleMs } = closed;
6167
- this.windows.push({ id, who, label, startedAt, lastWriteAt, idleMs, changes: /* @__PURE__ */ new Map() });
6168
- }
6169
- async firstOpen() {
6170
- const sweptAt = Date.now();
6171
- for (const file of listProjectFiles(this.dir)) {
6172
- const hash = await this.storeIfPresent(file.path);
6173
- if (this.whereFolder() !== "here") throw this.replaced();
6174
- if (hash !== null) this.tracked.set(file.path, { hash, stat: statKey(file, sweptAt) });
6175
- }
6176
- this.log.baseline = this.manifest();
6177
- this.persistLog();
6178
- this.saveStatCache();
6179
- }
6180
- manifest() {
6181
- return new Map([...this.tracked].map(([path, file]) => [path, file.hash]));
6182
- }
6183
- readStatCache() {
6184
- try {
6185
- const files = JSON.parse(readFileSync19(join20(this.home, "stat.json"), "utf-8"));
6186
- return new Map(Object.entries(files));
6187
- } catch {
6188
- return /* @__PURE__ */ new Map();
6189
- }
6190
- }
6191
- saveStatCache() {
6192
- const files = JSON.stringify(Object.fromEntries(this.tracked));
6193
- mkdirSync5(this.home, { recursive: true });
6194
- replaceFileAtomically(join20(this.home, "stat.json"), files, 420);
6195
- }
6196
- /**
6197
- * Takes in every write since the last sweep and files each change to its writer. ponytail: always the whole
6198
- * project (a stat per file, a hash only when the stat moved); per-path sweeps if projects reach tens of thousands.
6199
- */
6200
- async sweep() {
6201
- if (this.whereFolder() !== "here") return;
6202
- const sweptAt = Date.now();
6203
- const changedAt = (file) => Math.min(sweptAt, Math.max(file.mtimeMs, file.ctimeMs));
6204
- const seen = listProjectFiles(this.dir);
6205
- const present = new Set(seen.map((file) => file.path));
6206
- const removed = [...this.tracked.keys()].filter((path) => !present.has(path)).map((path) => {
6207
- const standing = this.standingAt(path);
6208
- return { at: standing ? changedAt(standing.stat) : sweptAt, path, file: null };
6209
- });
6210
- const appearedAt = (file) => Math.max(
6211
- changedAt(file),
6212
- ...removed.filter((gone) => blocks(this.nameKey(gone.path), this.nameKey(file.path))).map((gone) => gone.at)
6213
- );
6214
- const events = [
6215
- ...removed,
6216
- ...seen.map((file) => ({ at: appearedAt(file), path: file.path, file }))
6217
- ].sort((a, b) => a.at - b.at);
6218
- let changed = false;
6219
- for (const { at, path, file } of events) {
6220
- if (file) {
6221
- changed = await this.observe(path, statKey(file, sweptAt), at) || changed;
6222
- continue;
6223
- }
6224
- const known = this.tracked.get(path);
6225
- this.tracked.delete(path);
6226
- await this.record(path, known.hash, null, at);
6227
- changed = true;
6228
- }
6229
- if (changed) this.saveStatCache();
6230
- }
6231
- /** The file's hash once copied in; null when it was removed before the copy (the next sweep records that). */
6232
- async storeIfPresent(path) {
6233
- try {
6234
- return await this.blobs.put(join20(this.dir, path));
6235
- } catch (error) {
6236
- if (error.code === "ENOENT") return null;
6237
- throw error;
6238
- }
6239
- }
6240
- async observe(path, stat3, at) {
6241
- const known = this.tracked.get(path) ?? { hash: null, stat: null };
6242
- if (stat3 && known.stat === stat3) return false;
6243
- const hash = await this.storeIfPresent(path);
6244
- if (hash === null || this.whereFolder() !== "here") return false;
6245
- this.tracked.set(path, { hash, stat: stat3 });
6246
- if (known.hash !== hash) await this.record(path, known.hash, hash, at);
6247
- return known.hash !== hash || known.stat !== stat3;
6248
- }
6249
- async record(path, before, after, at) {
6250
- const endedBeforeThisWrite = this.windows.filter((open2) => !takesWrite(open2, at));
6251
- if (endedBeforeThisWrite.length) await this.commitOutside();
6252
- for (const window2 of endedBeforeThisWrite) await this.endWindow(window2);
6253
- const window = this.windows.at(-1);
6254
- if (window) this.touch(window, at);
6255
- addChange(window ?? this.outsideGroup(), path, before, after);
6256
- }
6257
- /** ponytail: another's write between Studio's write and its sweep folds into Studio's entry (per-write tokens). */
6258
- async claimNow(who, label, paths, { coalesceKey, idleMs, overwrote = {} }) {
6259
- await this.sweep();
6260
- const taken = await this.takeClaimed(who, paths, overwrote);
6261
- if (!taken.length) return this.claimNothing(coalesceKey);
6262
- const held = this.heldFor(coalesceKey, taken);
6263
- if (!held) await this.commitClaim();
6264
- await this.commitPending();
6265
- const group = held ?? this.newGroup(who, label);
6266
- for (const change of taken) addChange(group, change.path, change.before, change.after);
6267
- if (coalesceKey) return this.holdClaim(group, coalesceKey, idleMs);
6268
- const entry = await this.commit(group);
6269
- return entry && { id: entry.id };
6270
- }
6271
- /** A claim that took nothing still ends a held claim under another key. */
6272
- async claimNothing(coalesceKey) {
6273
- if (coalesceKey !== this.claimed?.key) await this.commitClaim();
6274
- return null;
6275
- }
6276
- /** Commits every other writer's pending changes, oldest first; open windows stay open. */
6277
- async commitPending() {
6278
- await this.commitOutside();
6279
- for (const window of [...this.windows])
6280
- if (window.changes.size) await this.commitCut(window, [...window.changes.keys()]);
6281
- }
6282
- pendingElsewhere() {
6283
- return Boolean(this.outside?.changes.size) || this.windows.some((open2) => open2.changes.size > 0);
6284
- }
6285
- /** The held claim `taken` continues: same key, each file from its own last change, no held file changed since. */
6286
- heldFor(key, taken) {
6287
- const group = key && this.claimed?.key === key && !this.pendingElsewhere() ? this.claimed.group : null;
6288
- const after = (path, before) => {
6289
- const own = group?.changes.get(path);
6290
- return own ? own.after : before;
6291
- };
6292
- const takenPaths = new Set(taken.map((change) => change.path));
6293
- const untouched = (change) => takenPaths.has(change.path) || (this.tracked.get(change.path)?.hash ?? null) === change.after;
6294
- return group && taken.every((change) => after(change.path, change.before) === change.before) && [...group.changes.values()].every(untouched) ? group : null;
6295
- }
6296
- logPath(path) {
6297
- return relative5(this.dir, resolve8(this.dir, path)).split(sep3).join("/");
6298
- }
6299
- /** Takes `paths`' uncommitted changes from others, cut at what `who` overwrote. */
6300
- async takeClaimed(who, paths, overwrote) {
6301
- const wanted = new Set(paths.map((path) => this.logPath(path)));
6302
- const at = new Map(
6303
- Object.entries(overwrote).map(([path, version]) => [
6304
- this.logPath(path),
6305
- hashOfVersion(version)
6306
- ])
6307
- );
6308
- const others = this.windows.filter((open2) => !sameWho(open2.who, who));
6309
- const groups = this.outside ? [this.outside, ...others] : others;
6310
- const hits = groups.flatMap(
6311
- (group) => [...group.changes.values()].filter((change) => wanted.has(change.path)).map((change) => ({ group, change }))
6312
- );
6313
- const cuts = [];
6314
- const walked = hits.map(() => /* @__PURE__ */ new Set());
6315
- for (const [i, { change }] of hits.entries()) {
6316
- const told = at.get(change.path);
6317
- cuts.push(await this.overwrittenBy(change, told, walked[i]) ?? told);
6318
- }
6319
- hits.forEach(
6320
- ({ change }, i) => forgetOverwrittenBytes(
6321
- join20(this.dir, change.path),
6322
- new Set([...walked[i]].map(hashVersion))
6323
- )
6324
- );
6325
- return hits.flatMap(({ group, change }, i) => this.cutOut(group, change, cuts[i]) ?? []);
6326
- }
6327
- /** Walks the server's writes back from `change.after` to the bytes they replaced, stored so an undo restores them. */
6328
- async overwrittenBy(change, told, seen) {
6329
- const absPath = join20(this.dir, change.path);
6330
- let bytes;
6331
- for (let hash = change.after; hash; ) {
6332
- if (seen.has(hash)) return void 0;
6333
- seen.add(hash);
6334
- const replaced = bytesOverwrittenBy(absPath, hashVersion(hash));
6335
- if (replaced === void 0) break;
6336
- bytes = replaced;
6337
- hash = hashOfVersion(fileContentVersion(replaced));
6338
- if (hash === change.before || hash === told) return hash;
6339
- }
6340
- if (bytes === void 0) return void 0;
6341
- const staged = join20(this.home, `overwrote-${randomUUID9()}`);
6342
- await writeFile(staged, bytes);
6343
- try {
6344
- return await this.blobs.put(staged);
6345
- } finally {
6346
- await rm3(staged, { force: true });
6347
- }
6348
- }
6349
- cutOut(group, change, cut) {
6350
- const [kept, claimed] = splitAt(change, cut, cut !== void 0 && this.blobs.has(cut));
6351
- group.changes.delete(change.path);
6352
- if (kept) group.changes.set(kept.path, kept);
6353
- return claimed;
6354
- }
6355
- /** Commits the window's cut `paths` as their own entry, other files staying: a window holds one change per file,
6356
- * so the writer's next write to a cut file would otherwise span the claimer's edit. */
6357
- async commitCut(window, paths) {
6358
- const part = { ...this.newGroup(window.who, window.label), startedAt: window.startedAt };
6359
- for (const path of paths) {
6360
- part.changes.set(path, window.changes.get(path));
6361
- window.changes.delete(path);
6362
- }
6363
- (window.parts ??= /* @__PURE__ */ new Set()).add(part.id);
6364
- window.entry = await this.commit(part);
6365
- }
6366
- holdClaim(group, key, idleMs = this.options.quietMs ?? 2e3) {
6367
- clearTimeout(this.claimed?.timer);
6368
- const timer = Number.isFinite(idleMs) ? setTimeout(() => this.background(() => this.commitClaim()), idleMs) : void 0;
6369
- timer?.unref?.();
6370
- this.claimed = { group, key, timer };
6371
- return group.changes.size ? { id: group.id } : null;
6372
- }
6373
- async commitClaim() {
6374
- const held = this.claimed;
6375
- this.claimed = null;
6376
- if (!held) return;
6377
- clearTimeout(held.timer);
6378
- await this.commit(held.group);
6379
- }
6380
- outsideGroup() {
6381
- const { quietMs = 2e3, maxGroupMs = 3e4 } = this.options;
6382
- const closeIn = (ms) => {
6383
- const timer = setTimeout(() => this.background(() => this.commitOutside()), ms);
6384
- timer.unref?.();
6385
- return timer;
6386
- };
6387
- if (!this.outside) {
6388
- this.outside = this.newGroup(OUTSIDE, OUTSIDE_LABEL);
6389
- this.maxTimer = closeIn(maxGroupMs);
6390
- }
6391
- clearTimeout(this.quietTimer);
6392
- this.quietTimer = closeIn(quietMs);
6393
- return this.outside;
6394
- }
6395
- newGroup(who, label) {
6396
- return { id: randomUUID9(), who, label, startedAt: this.now(), changes: /* @__PURE__ */ new Map() };
6397
- }
6398
- async commitOutside() {
6399
- if (this.outside?.changes.size) await this.commitClaim();
6400
- clearTimeout(this.quietTimer);
6401
- clearTimeout(this.maxTimer);
6402
- const group = this.outside;
6403
- this.outside = null;
6404
- if (group) await this.commit(group);
6405
- }
6406
- async commit(group, extra = {}) {
6407
- if (!group.changes.size) return null;
6408
- const pending = this.pendingEntry(group);
6409
- const endedAt = Math.max(pending.endedAt, this.log.entries.at(-1)?.endedAt ?? 0);
6410
- const entry = { ...pending, endedAt, ...extra };
6411
- this.log.entries.push(entry);
6412
- try {
6413
- this.persistLog({ type: "entry", entry });
6414
- } catch (error) {
6415
- this.log.entries.pop();
6416
- throw error;
6417
- }
6418
- for (const listener of this.listeners) listener(entry);
6419
- await this.keepWithinBudget();
6420
- return entry;
6421
- }
6422
- /** Stored bytes beyond what the current files need: a project larger than the budget still keeps its history. */
6423
- historyBytes() {
6424
- let current = 0;
6425
- for (const hash of new Set([...this.tracked.values()].map((file) => file.hash)))
6426
- current += this.blobs.size(hash);
6427
- return this.blobs.bytes() - current;
6428
- }
6429
- async keepWithinBudget() {
6430
- const budget = this.options.budgetBytes ?? 2 * 1024 ** 3;
6431
- let folded = false;
6432
- while (this.historyBytes() > budget && foldOldest(this.log)) {
6433
- folded = true;
6434
- await this.blobs.prune(
6435
- /* @__PURE__ */ new Set([...referencedHashes(this.log, this.manifest()), ...this.pendingHashes()])
6436
- );
6437
- }
6438
- if (folded) this.persistLog();
6439
- }
6440
- /** Hashes only pending changes point at yet, such as a claim's stored cut. */
6441
- pendingHashes() {
6442
- const changes = [...this.windows, this.outside, this.claimed?.group].flatMap((group) => [
6443
- ...group?.changes.values() ?? []
6444
- ]);
6445
- return changes.flatMap((change) => [change.before, change.after]).filter((hash) => hash !== null);
6446
- }
6447
- /** Before an operation or a window: every write is filed and every pending change committed, in write order. */
6448
- async settle() {
6449
- await this.sweep();
6450
- await this.commitClaim();
6451
- await this.commitPending();
6452
- }
6453
- /** A watcher saw a write: one sweep per burst takes it in (a deleted folder is reported by its name alone). */
6454
- noteChange(path) {
6455
- if (this.notedTimer || !affectsProjectSignature(this.dir, resolve8(this.dir, path))) return;
6456
- this.notedTimer = setTimeout(() => {
6457
- this.notedTimer = null;
6458
- this.background(() => this.sweep());
6459
- }, 20);
6460
- this.notedTimer.unref?.();
6461
- }
6462
- background(task) {
6463
- this.queue(task).catch((error) => {
6464
- if (!(error instanceof HistoryClosedError)) this.options.onError?.(error);
6465
- });
6466
- }
6467
- beginWindow(who, label, idleMs) {
6468
- return this.queue(async () => {
6469
- await this.settle();
6470
- const window = { ...this.newGroup(who, label), idleMs };
6471
- this.windows.push(window);
6472
- this.touch(window, Date.now());
6473
- const close = () => this.queue(() => this.sweepAndEnd(window));
6474
- return { id: window.id, startedAt: window.startedAt, close };
6475
- });
6476
- }
6477
- /** A window with no write for its idleMs ends, so a close that never comes cannot hold every later write. */
6478
- touch(window, at) {
6479
- window.lastWriteAt = Math.max(window.lastWriteAt ?? at, at);
6480
- clearTimeout(window.idleTimer);
6481
- if (window.idleMs === void 0 || !Number.isFinite(window.idleMs)) return;
6482
- window.idleTimer = setTimeout(
6483
- () => this.background(() => this.sweepAndEnd(window)),
6484
- window.idleMs
6485
- );
6486
- window.idleTimer.unref?.();
6487
- }
6488
- async sweepAndEnd(window) {
6489
- await this.sweep();
6490
- return this.endWindow(window);
6491
- }
6492
- /** Commits an open window once; ending it again returns the entry it became. */
6493
- async endWindow(window) {
6494
- if (!this.windows.includes(window)) return window.entry ?? null;
6495
- clearTimeout(window.idleTimer);
6496
- await this.commitClaim();
6497
- this.windows = this.windows.filter((open2) => open2 !== window);
6498
- window.entry = await this.commit(window) ?? window.entry ?? null;
6499
- return window.entry;
6500
- }
6501
- /** Every pending write, open window and outside group, committed: for flush and close. */
6502
- async settleAll() {
6503
- await this.sweep();
6504
- await this.commitClaim();
6505
- for (const window of [...this.windows]) await this.endWindow(window);
6506
- await this.commitOutside();
6507
- }
6508
- /** What took a removed path's place, dating its removal: an entry at the path, or a file at a folder above it. */
6509
- standingAt(path) {
6510
- const parts = path.split("/");
6511
- for (let depth = 1; depth <= parts.length; depth += 1) {
6512
- const at = parts.slice(0, depth).join("/");
6513
- const stat3 = lstatSync4(join20(this.dir, at), { throwIfNoEntry: false });
6514
- if (!stat3) return void 0;
6515
- if (at === path || !stat3.isDirectory()) return { at, stat: stat3 };
6516
- }
6517
- return void 0;
6518
- }
6519
- /** What a write of `path` would have to delete: a file or link at or above it, or a file in a folder at it. */
6520
- inTheWay(path, deleted) {
6521
- const standing = this.standingAt(path);
6522
- if (!standing) return void 0;
6523
- if (standing.stat.isSymbolicLink()) return standing.at;
6524
- if (standing.at === path && !standing.stat.isDirectory()) return void 0;
6525
- const real = realpathSync.native(join20(this.dir, standing.at));
6526
- const at = relative5(realpathSync.native(this.dir), real).split(sep3).join("/");
6527
- if (standing.at !== path) return deleted.has(at) ? void 0 : at;
6528
- return fileLeftIn(real, at, deleted);
6529
- }
6530
- /** Writes `target` (path to hash, null deletes) as one entry of `who`'s. */
6531
- async writeAs(who, label, target, extra) {
6532
- this.assertWritable();
6533
- const changes = [...target].filter(([path, hash]) => (this.tracked.get(path)?.hash ?? null) !== hash).sort(([, a], [, b]) => Number(a !== null) - Number(b !== null));
6534
- const deleted = new Set(changes.filter(([, hash]) => hash === null).map(([path]) => path));
6535
- const blocked = changes.filter(([, hash]) => hash !== null).map(([path]) => this.inTheWay(path, deleted)).find(Boolean);
6536
- if (blocked) throw new Error(`${blocked} is in the way; move or delete it, then try again.`);
6537
- const group = this.newGroup(who, label);
6538
- this.windows.push(group);
6539
- try {
6540
- for (const [path, hash] of changes) await this.writeProjectFile(path, hash);
6541
- await this.sweep();
6542
- } finally {
6543
- this.windows = this.windows.filter((open2) => open2 !== group);
6544
- }
6545
- return this.commit(group, extra);
6546
- }
6547
- /** Writes one project file (null deletes it), first leaving the running operation's receipt for its echo. */
6548
- async writeProjectFile(path, hash) {
6549
- this.assertWritable();
6550
- const absPath = join20(this.dir, path);
6551
- const { writeToken } = this;
6552
- if (writeToken) {
6553
- const version = hash === null ? DELETED_VERSION : hashVersion(hash);
6554
- recordFileWriteReceipt(absPath, { path, version, writeToken });
6555
- }
6556
- if (hash === null) {
6557
- this.assertWritable();
6558
- rmSync6(absPath, { force: true });
6559
- } else {
6560
- await removeEmptyFolders(absPath);
6561
- await this.blobs.writeTo(hash, absPath, () => this.assertWritable());
6562
- }
6563
- }
6564
- entry(id) {
6565
- const entry = this.log.entries.find((candidate) => candidate.id === id);
6566
- if (!entry) throw new Error("That change is no longer kept in this project's history.");
6567
- return entry;
6568
- }
6569
- undoLabel(entry) {
6570
- if (!entry.undoes) return `Undid: ${entry.label}`;
6571
- const original = this.log.entries.find((candidate) => candidate.id === entry.undoes);
6572
- return `Redid: ${original?.label ?? entry.label}`;
6573
- }
6574
- async undoNow(id, who, mode) {
6575
- const entry = this.entry(id);
6576
- const changed = this.movedOn(entry);
6577
- if (changed.length && !mode) return { ok: false, conflict: this.conflict(entry, changed) };
6578
- if (mode === "back-to-before") {
6579
- const index = this.log.entries.indexOf(entry);
6580
- const point = index > 0 ? this.log.entries[index - 1].id : START;
6581
- return {
6582
- ok: true,
6583
- entry: await this.restoreNow(point, who, `Went back to before: ${entry.label}`)
6584
- };
6585
- }
6586
- const target = new Map(
6587
- entry.files.filter((file) => mode !== "keep-later-edits" || !changed.includes(file)).map((file) => [file.path, file.before])
6588
- );
6589
- return {
6590
- ok: true,
6591
- entry: await this.writeAs(who, this.undoLabel(entry), target, { undoes: id })
6592
- };
6593
- }
6594
- movedOn(entry) {
6595
- return entry.files.filter((file) => (this.tracked.get(file.path)?.hash ?? null) !== file.after);
6596
- }
6597
- next(direction, who) {
6598
- const mine = (author) => sameWho(author, who) || sameWho(author, OUTSIDE);
6599
- const pending = [...[...this.windows].reverse(), this.outside, this.claimed?.group].find(
6600
- (group) => group?.changes.size && mine(group.who)
6601
- );
6602
- if (pending) return direction === "back" ? this.pendingEntry(pending) : void 0;
6603
- const everyone = this.options.undoScope === "everyone";
6604
- const ofOpenTurn = (entry) => this.windows.some((open2) => open2.parts?.has(entry.id));
6605
- return stepTarget(
6606
- this.log.entries,
6607
- direction,
6608
- (entry) => mine(entry.who) || everyone && !ofOpenTurn(entry)
6609
- );
6610
- }
6611
- /** A pending group as the entry it becomes once committed (a window's part gets a fresh id). */
6612
- pendingEntry(group) {
6613
- const files = [...group.changes.values()].sort((a, b) => a.path.localeCompare(b.path));
6614
- const { id, who, label, startedAt, lastWriteAt } = group;
6615
- return { id, who, label, startedAt, endedAt: lastWriteAt ?? this.now(), files };
6616
- }
6617
- conflict(entry, changed) {
6618
- const paths = new Set(changed.map((file) => file.path));
6619
- const after = this.log.entries.slice(this.log.entries.indexOf(entry) + 1);
6620
- const newer = after.filter((later) => later.files.some((file) => paths.has(file.path)));
6621
- return { files: [...paths], newer: newer.map((later) => later.id) };
6622
- }
6623
- async restoreNow(point, who, label) {
6624
- const files = manifestAt(this.log, point);
6625
- if (!files) throw new Error("That point is no longer kept in this project's history.");
6626
- const target = new Map(
6627
- [...this.tracked.keys()].map((path) => [path, null])
6628
- );
6629
- for (const [path, hash] of files) target.set(path, hash);
6630
- return this.writeAs(who, label, target, { restoredTo: point });
6631
- }
6632
- /** Settles, then runs `task` with its writes labelled `writeToken`. */
6633
- operation(writeToken, task) {
6634
- return this.queue(async () => {
6635
- await this.settle();
6636
- this.writeToken = writeToken;
6637
- try {
6638
- return await task();
6639
- } finally {
6640
- this.writeToken = void 0;
6641
- }
6642
- });
6643
- }
6644
- pointLabel(point) {
6645
- return point === START ? "the start" : this.entry(point).label;
6646
- }
6647
- api() {
6648
- return {
6649
- projectId: this.projectId,
6650
- beginWindow: (who, label, options = {}) => this.beginWindow(who, label, options.idleMs ?? this.options.maxGroupMs ?? 3e4),
6651
- claim: (who, label, paths, options = {}) => this.queue(() => this.claimNow(who, label, paths, options)),
6652
- noteChange: (path) => {
6653
- if (!this.closed) this.noteChange(path);
6654
- },
6655
- list: () => {
6656
- this.assertOpen();
6657
- const undone = undoneIds(this.log.entries);
6658
- return this.log.entries.map((entry) => ({
6659
- ...entry,
6660
- pinned: this.log.pins.has(entry.id),
6661
- undone: undone.has(entry.id)
6662
- }));
6663
- },
6664
- step: (direction, who, { writeToken } = {}) => this.operation(writeToken, async () => {
6665
- const target = this.next(direction, who);
6666
- return target ? this.undoNow(target.id, who) : { ok: true, entry: null };
6667
- }),
6668
- undo: (id, { who, mode, writeToken }) => this.operation(writeToken, () => this.undoNow(id, who, mode)),
6669
- restore: (point, who, { writeToken } = {}) => this.operation(
6670
- writeToken,
6671
- () => this.restoreNow(point, who, `Restored: ${this.pointLabel(point)}`)
6672
- ),
6673
- peek: (point) => {
6674
- this.assertOpen();
6675
- const files = manifestAt(this.log, point);
6676
- return files && Object.fromEntries(files);
6677
- },
6678
- checkout: (entryId, side, emptyDir) => this.queue(async () => {
6679
- this.entry(entryId);
6680
- if (!existsSync11(this.dir)) throw new Error(`The project folder is gone: ${this.dir}`);
6681
- const dest = resolve8(emptyDir);
6682
- if (isWithin(this.dir, dest))
6683
- throw new Error(`Checkout writes outside the project only: ${dest}`);
6684
- if ((await readdir3(dest).catch(missingIsEmpty)).length > 0)
6685
- throw new Error(`Checkout writes into an empty folder only: ${dest}`);
6686
- const files = manifestAround(this.log, entryId, side);
6687
- try {
6688
- for (const [path, hash] of files) await this.blobs.writeTo(hash, join20(dest, path));
6689
- } catch (error) {
6690
- for (const name of await readdir3(dest).catch(() => []))
6691
- await rm3(join20(dest, name), { recursive: true, force: true });
6692
- throw error;
6693
- }
6694
- }),
6695
- next: (direction, who) => {
6696
- this.assertOpen();
6697
- return this.next(direction, who);
6698
- },
6699
- readBlob: async (hash) => {
6700
- this.assertOpen();
6701
- return this.blobs.read(hash);
6702
- },
6703
- pin: (id, pinned) => {
6704
- this.assertOpen();
6705
- this.entry(id);
6706
- if (pinned) this.log.pins.add(id);
6707
- else this.log.pins.delete(id);
6708
- this.persistLog({ type: "pin", id, pinned });
6709
- },
6710
- onEntry: (listener) => {
6711
- this.listeners.add(listener);
6712
- return () => this.listeners.delete(listener);
6713
- },
6714
- flush: () => this.queue(() => this.settleAll()),
6715
- replacedAtPath: () => this.whereFolder() === "replaced",
6716
- close: () => this.closing ??= (async () => {
6717
- if (this.notedTimer) clearTimeout(this.notedTimer);
6718
- const settled = this.queue(() => this.settleAll());
6719
- this.closed = true;
6720
- await settled;
6721
- })()
6722
- };
6723
- }
6724
- };
6725
- function isWithin(dir, path) {
6726
- let probe = resolve8(path);
6727
- while (!existsSync11(probe) && dirname10(probe) !== probe) probe = dirname10(probe);
6728
- const rel = relative5(realpathSync.native(dir), realpathSync.native(probe));
6729
- return rel === "" || rel !== ".." && !rel.startsWith(`..${sep3}`) && !isAbsolute2(rel);
6730
- }
6731
- function missingIsEmpty(error) {
6732
- if (error.code === "ENOENT") return [];
6733
- throw error;
6734
- }
6735
- var HistoryClosedError = class extends Error {
6736
- constructor(message) {
6737
- super(message);
6738
- this.name = "HistoryClosedError";
6739
- }
6740
- };
6741
- async function openProjectHistory(options) {
6742
- if (!existsSync11(options.projectDir))
6743
- throw new HistoryClosedError(`${options.projectDir} is gone or now another project.`);
6744
- const projectId = projectHistoryId(options.projectDir, options.historyRoot);
6745
- const release = await takeHistoryOwnership(
6746
- join20(options.historyRoot, projectId),
6747
- options.ownerWaitMs ?? 5e3
6748
- );
6749
- try {
6750
- const blobs = await openBlobStore(join20(options.historyRoot, projectId, "blobs"));
6751
- const engine = new Engine(options, projectId, blobs);
6752
- await engine.queue(() => engine.start());
6753
- pruneGoneProjectHistoriesDaily(options.historyRoot, engine.now(), {
6754
- onError: options.onError,
6755
- budgetMs: options.pruneGoneProjectsBudgetMs
6756
- });
6757
- const api = engine.api();
6758
- return {
6759
- ...api,
6760
- close: () => api.close().finally(release)
6761
- };
6762
- } catch (error) {
6763
- release();
6764
- throw error;
6765
- }
6766
- }
6767
-
6768
5197
  // src/routes/history.ts
6769
5198
  var YOU = { kind: "person", name: "You" };
6770
5199
  async function historyOf(adapter, c) {
@@ -6936,33 +5365,9 @@ function createStudioApi(adapter) {
6936
5365
  return api;
6937
5366
  }
6938
5367
 
6939
- // src/history/historyCache.ts
6940
- function historyCache(open2) {
6941
- const histories = /* @__PURE__ */ new Map();
6942
- const get = async (projectDir) => {
6943
- let cached = histories.get(projectDir);
6944
- if (!cached) histories.set(projectDir, cached = open2(projectDir));
6945
- const opened = await cached;
6946
- if (!opened?.replacedAtPath()) return opened;
6947
- const reopen = () => open2(projectDir);
6948
- if (histories.get(projectDir) === cached)
6949
- histories.set(projectDir, opened.close().then(reopen, reopen));
6950
- return get(projectDir);
6951
- };
6952
- return {
6953
- get,
6954
- /** The history if one was opened, without opening it. */
6955
- peek: (projectDir) => histories.get(projectDir),
6956
- forget: (projectDir) => histories.delete(projectDir),
6957
- closeAll: () => Promise.all(
6958
- [...histories.values()].map((opened) => opened.then((history) => history?.close()))
6959
- )
6960
- };
6961
- }
6962
-
6963
5368
  // src/helpers/duplicateElement.ts
6964
5369
  import { sameInstant } from "@hyperframes/core/clip-facts";
6965
- import { ensureHfIds as ensureHfIds4 } from "@hyperframes/parsers/hf-ids";
5370
+ import { ensureHfIds as ensureHfIds3 } from "@hyperframes/parsers/hf-ids";
6966
5371
  function duplicateElementInHtml(source, target, newId, at) {
6967
5372
  const { document, wrappedFragment } = parseSourceDocument(source);
6968
5373
  const element = findTargetElement(document, target);
@@ -6982,7 +5387,7 @@ function duplicateElementInHtml(source, target, newId, at) {
6982
5387
  clone.setAttribute("data-start", String(at));
6983
5388
  element.parentElement.insertBefore(clone, element.nextSibling);
6984
5389
  const html = wrappedFragment ? document.body.innerHTML || "" : document.toString();
6985
- return { html: ensureHfIds4(html), matched: true, newId: uniqueId };
5390
+ return { html: ensureHfIds3(html), matched: true, newId: uniqueId };
6986
5391
  }
6987
5392
  function nextUniqueId(document, newId) {
6988
5393
  let uniqueId = newId;
@@ -7014,17 +5419,17 @@ function numericAttribute(element, name) {
7014
5419
  }
7015
5420
 
7016
5421
  // src/helpers/previewDocumentStore.ts
7017
- import { createHash as createHash8 } from "crypto";
7018
- import { readFileSync as readFileSync20, readdirSync as readdirSync8, rmSync as rmSync7 } from "fs";
7019
- import { join as join21 } from "path";
5422
+ import { createHash as createHash4 } from "crypto";
5423
+ import { readFileSync as readFileSync13, readdirSync as readdirSync5, rmSync as rmSync5 } from "fs";
5424
+ import { join as join15 } from "path";
7020
5425
  function createPreviewDocumentStore(projectDir, salt) {
7021
- const dir = join21(projectDir, ".hyperframes", "preview");
7022
- const fileFor = (key) => join21(dir, `${createHash8("sha256").update(`${salt}
5426
+ const dir = join15(projectDir, ".hyperframes", "preview");
5427
+ const fileFor = (key) => join15(dir, `${createHash4("sha256").update(`${salt}
7023
5428
  ${key}`).digest("hex").slice(0, 32)}.html`);
7024
5429
  return {
7025
5430
  read(key) {
7026
5431
  try {
7027
- return readFileSync20(fileFor(key), "utf-8");
5432
+ return readFileSync13(fileFor(key), "utf-8");
7028
5433
  } catch {
7029
5434
  return null;
7030
5435
  }
@@ -7034,8 +5439,8 @@ ${key}`).digest("hex").slice(0, 32)}.html`);
7034
5439
  mkdirWithinProject(projectDir, dir);
7035
5440
  const file = fileFor(key);
7036
5441
  replaceFileAtomically(file, html, 420);
7037
- for (const name of readdirSync8(dir)) {
7038
- if (join21(dir, name) !== file) rmSync7(join21(dir, name), { force: true });
5442
+ for (const name of readdirSync5(dir)) {
5443
+ if (join15(dir, name) !== file) rmSync5(join15(dir, name), { force: true });
7039
5444
  }
7040
5445
  } catch {
7041
5446
  }