fix(studio,producer): post-merge regressions and open issues (#1643)

- fix gsapDragCommit.test.ts import for commitGsapPositionFromDrag
- add generateId() helper with insecure-context fallback (fixes #1637)
- enable STUDIO_KEYFRAMES_ENABLED by default
- prevent clock duration from shrinking during playback (fixes #1636)
- add maxRetries to rmSync cleanup calls to handle ENOTEMPTY race (fixes #1644)
This commit is contained in:
Miguel Ángel
2026-06-22 11:19:52 -04:00
committed by GitHub
parent 4db708eb4a
commit 1f5dbdbc4c
12 changed files with 31 additions and 19 deletions
+6 -7
View File
@@ -2259,15 +2259,14 @@ export function initSandboxRuntimeModular(): void {
bindMediaMetadataListeners();
}
// Keep clock duration in sync with the resolved timeline duration.
// Catches async timeline rebinds that happen outside the 60-tick
// branch (metadata hydration, deferred setTimeout). Note: this reads
// the DOM each tick (duration floors query authored windows + the
// root's declared data-duration), which also keeps live edits to
// data-duration in the studio reflected without a rebind.
// Sync clock duration with the resolved timeline each tick (catches async
// rebinds, live data-duration edits). Never shrink while playing — transient
// short reads cause reachedEnd() → playhead jumps to end (#1636).
if (state.capturedTimeline) {
const dur = getSafeTimelineDurationSeconds(state.capturedTimeline, 0);
if (dur > 0) clock.setDuration(dur);
if (dur > 0 && (!clock.isPlaying() || dur >= clock.getDuration())) {
clock.setDuration(dur);
}
}
// Audio-master clock: three tiers of timing precision.
@@ -707,7 +707,7 @@ export async function renderChunk(
// Clean up only after the hash + perf sidecar landed. Any failure above
// leaves the framesDir in place for inspection.
try {
rmSync(workDir, { recursive: true, force: true });
rmSync(workDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
} catch (err) {
log.warn("[renderChunk] failed to remove work dir", {
workDir,
@@ -65,7 +65,7 @@ export async function cleanupRenderResources(input: {
// `force: true` swallows ENOENT, so no need to existsSync first.
await safeCleanup(
`remove workDir (${label})`,
() => rmSync(workDir, { recursive: true, force: true }),
() => rmSync(workDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 }),
log,
);
}
@@ -1699,7 +1699,7 @@ export async function executeRenderJob(
await safeCleanup(
"remove workDir",
() => {
rmSync(workDir, { recursive: true, force: true });
rmSync(workDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
},
log,
);
@@ -73,7 +73,7 @@ export const STUDIO_COLOR_GRADING_ENABLED = resolveStudioBooleanEnvFlag(
export const STUDIO_KEYFRAMES_ENABLED = resolveStudioBooleanEnvFlag(
env,
["VITE_STUDIO_ENABLE_KEYFRAMES", "VITE_STUDIO_KEYFRAMES_ENABLED"],
false,
true,
);
export const STUDIO_RAZOR_TOOL_ENABLED = resolveStudioBooleanEnvFlag(
@@ -20,6 +20,7 @@ import type { SlideshowManifest, SlideHotspot } from "@hyperframes/core/slidesho
import { usePlayerStore } from "../../player";
import { useDomEditSelectionContext } from "../../contexts/DomEditContext";
import { useFileManagerContext } from "../../contexts/FileManagerContext";
import { generateId } from "../../utils/generateId";
import {
SectionHeader,
SlideList,
@@ -325,7 +326,7 @@ export function SlideshowPanel({ scenes, onPersist, onPersistNotes }: SlideshowP
const handleCreateSequence = useCallback(
(label: string) => {
const id = `seq-${crypto.randomUUID()}`;
const id = `seq-${generateId()}`;
applyManifest(createSequence(manifestRef.current, id, label)).catch(() => {});
},
[applyManifest],
@@ -7,6 +7,7 @@ import { useState, useCallback, useId } from "react";
import type { SlideRef, SlideHotspot, SlideSequence } from "@hyperframes/core/slideshow";
import type { DomEditSelection } from "../editor/domEditing";
import type { SceneInfo } from "./slideshowPanelHelpers";
import { generateId } from "../../utils/generateId";
// ── Section header (accordion toggle) ────────────────────────────────────
@@ -425,7 +426,7 @@ export function HotspotTool({
// fallow-ignore-next-line complexity
const handleMakeHotspot = useCallback(() => {
if (!selectedSceneId || !targetSequenceId || !elementKey) return;
const id = `hotspot-${elementKey}-${crypto.randomUUID()}`;
const id = `hotspot-${elementKey}-${generateId()}`;
const label = hotspotLabel.trim() || elementKey;
onAddHotspot(selectedSceneId, { id, label, target: targetSequenceId });
setHotspotLabel("");
@@ -1,6 +1,7 @@
import { useState, useEffect, useCallback, useRef, useMemo } from "react";
import { trackStudioRenderStart } from "../../telemetry/events";
import { getAnonymousId } from "../../telemetry/config";
import { generateId } from "../../utils/generateId";
export interface RenderJob {
id: string;
@@ -131,7 +132,7 @@ export function useRenderQueue(projectId: string | null) {
});
} catch {
const failedJob: RenderJob = {
id: crypto.randomUUID(),
id: generateId(),
status: "failed",
progress: 0,
error: "Could not reach render server. Use `hyperframes render` from the CLI instead.",
@@ -143,7 +144,7 @@ export function useRenderQueue(projectId: string | null) {
}
if (!res.ok) {
const failedJob: RenderJob = {
id: crypto.randomUUID(),
id: generateId(),
status: "failed",
progress: 0,
error: `Server error (${res.status}). Check the terminal for details.`,
@@ -1,13 +1,13 @@
import { describe, expect, it, beforeEach } from "vitest";
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
import type { DomEditSelection } from "../components/editor/domEditingTypes";
import { commitGsapPositionFromDrag } from "./gsapDragPositionCommit";
import {
commitStaticGsapPosition,
commitStaticGsapRotation,
parkPlayheadOnKeyframe,
type GsapDragCommitCallbacks,
} from "./gsapDragCommit";
import { commitGsapPositionFromDrag } from "./gsapDragPositionCommit";
import { usePlayerStore } from "../player/store/playerStore";
// Minimal selection whose element has no drag-baseline attributes (origX/Y = 0).
+3 -2
View File
@@ -5,6 +5,8 @@
// localStorage.setItem('hyperframes-studio:telemetryDisabled','1')
// ---------------------------------------------------------------------------
import { generateId } from "../utils/generateId";
const ANON_ID_KEY = "hyperframes-studio:anonymousId";
const OPT_OUT_KEY = "hyperframes-studio:telemetryDisabled";
const NOTICE_KEY = "hyperframes-studio:telemetryNoticeShown";
@@ -18,8 +20,7 @@ function safeLocalStorage(): Storage | null {
}
function newAnonymousId(): string {
if (typeof crypto !== "undefined" && "randomUUID" in crypto) return crypto.randomUUID();
return `anon-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
return generateId();
}
export function getAnonymousId(): string {
+7
View File
@@ -0,0 +1,7 @@
// ponytail: crypto.randomUUID is undefined on plain HTTP non-localhost origins
export function generateId(): string {
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
return crypto.randomUUID();
}
return `${Date.now()}-${Math.random().toString(36).slice(2)}`;
}
+3 -1
View File
@@ -1,3 +1,5 @@
import { generateId } from "./generateId";
// PostHog public ingest key — write-only, safe to ship in the client bundle
const POSTHOG_API_KEY = "phc_zjjbX0PnWxERXrMHhkEJWj9A9BhGVLRReICgsfTMmpx";
const POSTHOG_HOST = "https://us.i.posthog.com";
@@ -29,7 +31,7 @@ function getDistinctId(): string {
} catch {
// localStorage may be unavailable
}
distinctId = crypto.randomUUID();
distinctId = generateId();
try {
localStorage.setItem("hf-studio-anon-id", distinctId);
} catch {