@hyperframes/studio-server 0.8.170 → 0.8.171

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.
@@ -107,15 +107,17 @@ import { replaceFileAtomically } from "@hyperframes/core/atomic-file";
107
107
 
108
108
  // src/history/ownerLock.ts
109
109
  import { randomUUID as randomUUID2 } from "crypto";
110
- import { linkSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
110
+ import { linkSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "fs";
111
111
  import { join } from "path";
112
112
  var HistoryBusyError = class extends Error {
113
- constructor(pid) {
113
+ constructor(pid, url) {
114
114
  super(`This project's history is open in another process (pid ${pid}).`);
115
115
  this.pid = pid;
116
+ this.url = url;
116
117
  this.name = "HistoryBusyError";
117
118
  }
118
119
  pid;
120
+ url;
119
121
  };
120
122
  function alive(pid) {
121
123
  if (!Number.isInteger(pid) || pid <= 0) return false;
@@ -166,14 +168,32 @@ function evictDeadOwner(file) {
166
168
  releaseOwn(evictor);
167
169
  }
168
170
  }
169
- async function takeHistoryOwnership(home, waitMs) {
171
+ function advertise(urlFile, url) {
172
+ if (!url) return rmSync(urlFile, { force: true });
173
+ const draft = `${urlFile}-${process.pid}-${randomUUID2()}.tmp`;
174
+ writeFileSync(draft, JSON.stringify({ pid: process.pid, url }));
175
+ renameSync(draft, urlFile);
176
+ }
177
+ function servedBy(file, urlFile, pid) {
178
+ try {
179
+ const said = JSON.parse(readFileSync(urlFile, "utf-8"));
180
+ const live = said.pid === pid && alive(pid) && ownerOf(file) === pid;
181
+ return live && typeof said.url === "string" ? said.url : void 0;
182
+ } catch {
183
+ return void 0;
184
+ }
185
+ }
186
+ async function takeHistoryOwnership(home, waitMs, url) {
170
187
  const file = join(home, "owner.pid");
188
+ const urlFile = join(home, "owner-url.json");
171
189
  const deadline = Date.now() + waitMs;
172
190
  mkdirSync(home, { recursive: true });
173
191
  for (; ; ) {
174
192
  if (claim(file)) {
193
+ advertise(urlFile, url);
175
194
  let held = true;
176
195
  return () => {
196
+ if (held && servedBy(file, urlFile, process.pid)) rmSync(urlFile, { force: true });
177
197
  if (held) releaseOwn(file);
178
198
  held = false;
179
199
  };
@@ -181,7 +201,7 @@ async function takeHistoryOwnership(home, waitMs) {
181
201
  const pid = ownerOf(file);
182
202
  if (pid === null) continue;
183
203
  if (!alive(pid) && evictDeadOwner(file)) continue;
184
- if (Date.now() >= deadline) throw new HistoryBusyError(pid);
204
+ if (Date.now() >= deadline) throw new HistoryBusyError(pid, servedBy(file, urlFile, pid));
185
205
  await new Promise((settle) => setTimeout(settle, 50));
186
206
  }
187
207
  }
@@ -411,7 +431,7 @@ function createProjectSignature(projectDir, excluding = /* @__PURE__ */ new Set(
411
431
 
412
432
  // src/history/blobStore.ts
413
433
  import { createHash as createHash3, randomUUID as randomUUID3 } from "crypto";
414
- import { constants, createReadStream, createWriteStream, renameSync } from "fs";
434
+ import { constants, createReadStream, createWriteStream, renameSync as renameSync2 } from "fs";
415
435
  import { copyFile, mkdir, readdir, readFile, rename, rm, stat } from "fs/promises";
416
436
  import { dirname, join as join2 } from "path";
417
437
  import { pipeline } from "stream/promises";
@@ -441,7 +461,7 @@ async function cloneOrCopy(from, to, beforeReplace) {
441
461
  try {
442
462
  await copyFile(from, temp, constants.COPYFILE_FICLONE);
443
463
  beforeReplace?.();
444
- renameSync(temp, to);
464
+ renameSync2(temp, to);
445
465
  } catch (error) {
446
466
  await rm(temp, { force: true });
447
467
  throw error;
@@ -493,7 +513,7 @@ async function openBlobStore(dir) {
493
513
 
494
514
  // src/history/pruneHistories.ts
495
515
  import { randomUUID as randomUUID5 } from "crypto";
496
- import { readdirSync as readdirSync2, readFileSync as readFileSync5, renameSync as renameSync2, statSync as statSync2, writeFileSync as writeFileSync3 } from "fs";
516
+ import { readdirSync as readdirSync2, readFileSync as readFileSync5, renameSync as renameSync3, statSync as statSync2, writeFileSync as writeFileSync3 } from "fs";
497
517
  import { lstat, readdir as readdir2, rm as rm2 } from "fs/promises";
498
518
  import { tmpdir } from "os";
499
519
  import { dirname as dirname2, join as join4 } from "path";
@@ -657,7 +677,7 @@ function recheckAndMoveAway(historyRoot, id, { now, dryRun, tempDir }) {
657
677
  if (projectDir === null) return null;
658
678
  if (dryRun) return { projectDir, trash: null };
659
679
  const trash = join4(historyRoot, `${PRUNED_PREFIX}${id}-${randomUUID5()}`);
660
- renameSync2(home, trash);
680
+ renameSync3(home, trash);
661
681
  return { projectDir, trash };
662
682
  }
663
683
  function abandonedProject(home, id, now, tempDir) {
@@ -716,7 +736,7 @@ function pruneGoneProjectHistoriesDaily(historyRoot, now, { onError, budgetMs }
716
736
 
717
737
  // src/history/videoState.ts
718
738
  import { createHash as createHash5, randomUUID as randomUUID6 } from "crypto";
719
- import { createReadStream as createReadStream2, existsSync as existsSync2, readFileSync as readFileSync8, renameSync as renameSync3, statSync as statSync3 } from "fs";
739
+ import { createReadStream as createReadStream2, existsSync as existsSync2, readFileSync as readFileSync8, renameSync as renameSync4, statSync as statSync3 } from "fs";
720
740
  import { isAbsolute as isAbsolute4, join as join7, resolve as resolve6 } from "path";
721
741
  import { replaceFileAtomically as replaceFileAtomically3 } from "@hyperframes/core/atomic-file";
722
742
 
@@ -1117,7 +1137,7 @@ async function updateState(dir, update) {
1117
1137
  state = readState(dir);
1118
1138
  } catch (error) {
1119
1139
  if (!(error instanceof SyntaxError)) throw error;
1120
- renameSync3(statePath(dir), `${statePath(dir)}.invalid-${randomUUID6()}`);
1140
+ renameSync4(statePath(dir), `${statePath(dir)}.invalid-${randomUUID6()}`);
1121
1141
  state = emptyState();
1122
1142
  }
1123
1143
  update(state);
@@ -2406,7 +2426,8 @@ async function openProjectHistory(options) {
2406
2426
  const projectId = projectHistoryId(options.projectDir, options.historyRoot);
2407
2427
  const release = await takeHistoryOwnership(
2408
2428
  join8(options.historyRoot, projectId),
2409
- options.ownerWaitMs ?? 5e3
2429
+ options.ownerWaitMs ?? 5e3,
2430
+ options.ownerUrl
2410
2431
  );
2411
2432
  try {
2412
2433
  const blobs = await openBlobStore(join8(options.historyRoot, projectId, "blobs"));
@@ -1,6 +1,6 @@
1
1
  import '@hyperframes/core';
2
- export { a as PreviewApiAdapter, P as PreviewDocumentStore, i as injectMediaCodecMap, f as injectMediaCodecMapIntoHtml, g as isAutoProxyEnabled, p as proxyEtagSalt, r as resolvePreviewMediaCodecProbeCache } from '../mediaProxyPreview-J8vlAvrx.js';
2
+ export { a as PreviewApiAdapter, P as PreviewDocumentStore, i as injectMediaCodecMap, f as injectMediaCodecMapIntoHtml, g as isAutoProxyEnabled, p as proxyEtagSalt, r as resolvePreviewMediaCodecProbeCache } from '../mediaProxyPreview-6bL5skob.js';
3
3
  import './mediaCodecMap.js';
4
4
  import '@hyperframes/parsers';
5
5
  import '@hyperframes/core/compiler';
6
- import '../projectHistory-C5dpCjPU.js';
6
+ import '../projectHistory-ByC9sa3X.js';
@@ -1,9 +1,10 @@
1
- import { P as ProjectHistory, f as HistoryTarget, g as HistoryWho } from '../projectHistory-C5dpCjPU.js';
2
- export { C as ClosedWindow, D as DEFAULT_HISTORY_ROOT, S as HISTORY_START, H as HistoryClosedError, a as HistoryEntry, b as HistoryEntrySide, c as HistoryFileChange, d as HistoryListItem, e as HistoryResult, h as HistoryWindow, M as MAX_WINDOW_IDLE_MS, i as ProjectHistoryOptions, j as ProjectHistoryView, U as UNDO_MODES, k as UndoMode, o as openProjectHistory, r as readProjectHistory } from '../projectHistory-C5dpCjPU.js';
1
+ import { P as ProjectHistory, f as HistoryTarget, g as HistoryWho } from '../projectHistory-ByC9sa3X.js';
2
+ export { C as ClosedWindow, D as DEFAULT_HISTORY_ROOT, S as HISTORY_START, H as HistoryClosedError, a as HistoryEntry, b as HistoryEntrySide, c as HistoryFileChange, d as HistoryListItem, e as HistoryResult, h as HistoryWindow, M as MAX_WINDOW_IDLE_MS, i as ProjectHistoryOptions, j as ProjectHistoryView, U as UNDO_MODES, k as UndoMode, o as openProjectHistory, r as readProjectHistory } from '../projectHistory-ByC9sa3X.js';
3
3
 
4
4
  declare class HistoryBusyError extends Error {
5
5
  readonly pid: number;
6
- constructor(pid: number);
6
+ readonly url?: string | undefined;
7
+ constructor(pid: number, url?: string | undefined);
7
8
  }
8
9
 
9
10
  declare class HistoryIdError extends Error {
@@ -20,7 +20,7 @@ import {
20
20
  recordRenderSuccess,
21
21
  registerRenderOutput,
22
22
  retainSnapshotOutput
23
- } from "../chunk-SZF3L5MT.js";
23
+ } from "../chunk-GG4AS4RK.js";
24
24
  import "../chunk-K56J3CL7.js";
25
25
  import "../chunk-SVQFE44C.js";
26
26
  import "../chunk-L3HJVTQE.js";
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { Hono } from 'hono';
2
- import { S as StudioApiAdapter, P as PreviewDocumentStore, M as MediaProcessingJobState } from './mediaProxyPreview-J8vlAvrx.js';
3
- export { L as LintResult, a as PreviewApiAdapter, R as RenderJobState, b as ResolvedProject, c as StudioSelectionResponse, d as StudioSelectionSnapshot, e as StudioSelectionTextField } from './mediaProxyPreview-J8vlAvrx.js';
4
- export { C as ClosedWindow, D as DEFAULT_HISTORY_ROOT, S as HISTORY_START, H as HistoryClosedError, a as HistoryEntry, b as HistoryEntrySide, c as HistoryFileChange, d as HistoryListItem, e as HistoryResult, f as HistoryTarget, g as HistoryWho, h as HistoryWindow, M as MAX_WINDOW_IDLE_MS, P as ProjectHistory, i as ProjectHistoryOptions, j as ProjectHistoryView, U as UNDO_MODES, k as UndoMode, o as openProjectHistory, r as readProjectHistory } from './projectHistory-C5dpCjPU.js';
2
+ import { S as StudioApiAdapter, P as PreviewDocumentStore, M as MediaProcessingJobState } from './mediaProxyPreview-6bL5skob.js';
3
+ export { L as LintResult, a as PreviewApiAdapter, R as RenderJobState, b as ResolvedProject, c as StudioSelectionResponse, d as StudioSelectionSnapshot, e as StudioSelectionTextField } from './mediaProxyPreview-6bL5skob.js';
4
+ export { C as ClosedWindow, D as DEFAULT_HISTORY_ROOT, S as HISTORY_START, H as HistoryClosedError, a as HistoryEntry, b as HistoryEntrySide, c as HistoryFileChange, d as HistoryListItem, e as HistoryResult, f as HistoryTarget, g as HistoryWho, h as HistoryWindow, M as MAX_WINDOW_IDLE_MS, P as ProjectHistory, i as ProjectHistoryOptions, j as ProjectHistoryView, U as UNDO_MODES, k as UndoMode, o as openProjectHistory, r as readProjectHistory } from './projectHistory-ByC9sa3X.js';
5
5
  export { ArtifactCapture, ArtifactInput, ArtifactRecord, HistoryBusyError, HistoryIdError, ProjectHistoryRecord, PrunedHistory, RenderRecord, VideoState, artifactDirectory, beginArtifactCapture, finishSnapshotRun, forgetRecordedArtifacts, historyCache, listProjectHistories, pruneGoneProjectHistories, recordArtifact, recordArtifacts, recordRenderSuccess, registerRenderOutput, retainSnapshotOutput } from './history/index.js';
6
6
  export { CACHE_DIR_NAME as PROXY_CACHE_DIR_NAME } from './helpers/proxyTranscoder.js';
7
7
  export { walkDir } from './helpers/safePath.js';
package/dist/index.js CHANGED
@@ -60,7 +60,7 @@ import {
60
60
  resolveProjectSignature,
61
61
  retainSnapshotOutput,
62
62
  settledFileTag
63
- } from "./chunk-SZF3L5MT.js";
63
+ } from "./chunk-GG4AS4RK.js";
64
64
  import {
65
65
  clearElementScreenshotIsolation,
66
66
  getElementScreenshotClip
@@ -6409,6 +6409,33 @@ function nextStep(history, direction) {
6409
6409
  const paths = target.files.map((file) => file.path);
6410
6410
  return { id: target.id, label: target.label, endedAt: target.endedAt, paths };
6411
6411
  }
6412
+ var LOOPBACK = /* @__PURE__ */ new Set(["127.0.0.1", "localhost", "[::1]"]);
6413
+ var FORWARD_DEADLINE_MS = 5e3;
6414
+ function ownerRoute(c, error) {
6415
+ if (!(error instanceof HistoryBusyError) || !error.url || !URL.canParse(error.url)) return null;
6416
+ const owner = new URL(error.url);
6417
+ if (owner.protocol !== "http:" || !LOOPBACK.has(owner.hostname)) return null;
6418
+ const rest = c.req.path.replace(/^.*?\/projects\/[^/]+\/history/, "");
6419
+ return `${owner.href.replace(/\/$/, "")}${rest}${new URL(c.req.url).search}`;
6420
+ }
6421
+ async function forward(c, target) {
6422
+ const token = c.req.header("X-Hyperframes-Write-Token");
6423
+ const reads = c.req.method === "GET" || c.req.method === "HEAD";
6424
+ const reply = await fetch(target, {
6425
+ method: c.req.method,
6426
+ headers: {
6427
+ "content-type": c.req.header("content-type") ?? "application/json",
6428
+ ...token && { "X-Hyperframes-Write-Token": token }
6429
+ },
6430
+ ...!reads && { body: await c.req.arrayBuffer() },
6431
+ signal: AbortSignal.timeout(FORWARD_DEADLINE_MS)
6432
+ });
6433
+ const type = reply.headers.get("content-type") ?? "application/json";
6434
+ return new Response(await reply.arrayBuffer(), {
6435
+ status: reply.status,
6436
+ headers: { "content-type": type }
6437
+ });
6438
+ }
6412
6439
  async function withHistory(adapter, c, task) {
6413
6440
  try {
6414
6441
  const history = await historyOf(adapter, c);
@@ -6416,7 +6443,9 @@ async function withHistory(adapter, c, task) {
6416
6443
  const result = await task(history, await bodyOf(c));
6417
6444
  return result instanceof Response ? result : c.json(result ?? null);
6418
6445
  } catch (error) {
6419
- return c.json({ error: error instanceof Error ? error.message : String(error) }, 409);
6446
+ const target = ownerRoute(c, error);
6447
+ const forwarded = target && await forward(c, target).catch(() => null);
6448
+ return forwarded || c.json({ error: error instanceof Error ? error.message : String(error) }, 409);
6420
6449
  }
6421
6450
  }
6422
6451
  function registerHistoryRoutes(api, adapter) {
@@ -2,7 +2,7 @@ import * as _hyperframes_core from '@hyperframes/core';
2
2
  import { RegistryItem, PreviewProxyBox } from '@hyperframes/core';
3
3
  import { CanvasResolution } from '@hyperframes/parsers';
4
4
  import { BundleOptions } from '@hyperframes/core/compiler';
5
- import { P as ProjectHistory } from './projectHistory-C5dpCjPU.js';
5
+ import { P as ProjectHistory } from './projectHistory-ByC9sa3X.js';
6
6
  import { MediaCodecProbeCache, HtmlSourceLike } from './helpers/mediaCodecMap.js';
7
7
 
8
8
  /** Resolved info about a single project. */
@@ -50,6 +50,7 @@ interface ProjectHistoryOptions {
50
50
  onError?: (error: unknown) => void;
51
51
  /** How long an open waits for another process to close the same history (default 5 s). */
52
52
  ownerWaitMs?: number;
53
+ ownerUrl?: string;
53
54
  /** A CLI turn's window from an earlier open: writes since, within its idle limit, become the entry with its id. */
54
55
  closedWindow?: ClosedWindow;
55
56
  closedWindowAfterOwnership?: () => ClosedWindow | undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperframes/studio-server",
3
- "version": "0.8.170",
3
+ "version": "0.8.171",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/heygen-com/hyperframes",
@@ -85,8 +85,8 @@
85
85
  "postcss": "^8.5.8",
86
86
  "postcss-selector-parser": "^7.1.2",
87
87
  "sharp": "^0.35.0",
88
- "@hyperframes/core": "0.8.170",
89
- "@hyperframes/parsers": "0.8.170"
88
+ "@hyperframes/core": "0.8.171",
89
+ "@hyperframes/parsers": "0.8.171"
90
90
  },
91
91
  "devDependencies": {
92
92
  "@types/node": "^25.0.10",