@hyperframes/studio 0.8.170 → 0.8.172

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.
@@ -1146,7 +1146,8 @@ var Player = forwardRef(
1146
1146
  onPainted,
1147
1147
  portrait,
1148
1148
  style,
1149
- suppressLoadingOverlay
1149
+ suppressLoadingOverlay,
1150
+ noLoadTimeout
1150
1151
  }, ref) => {
1151
1152
  const containerRef = useRef(null);
1152
1153
  const loadCountRef = useRef(0);
@@ -1204,6 +1205,7 @@ var Player = forwardRef(
1204
1205
  setCompositionLoading(true);
1205
1206
  loadStartedAtRef.current = performance.now();
1206
1207
  loadIdRef.current += 1;
1208
+ player.removeAttribute("no-load-timeout");
1207
1209
  player.setAttribute("src", retryUrl.pathname + retryUrl.search);
1208
1210
  };
1209
1211
  retryPreviewRef.current = retryPreview;
@@ -1286,6 +1288,7 @@ var Player = forwardRef(
1286
1288
  player.setAttribute("shader-capture-scale", "1");
1287
1289
  player.setAttribute("shader-loading", "player");
1288
1290
  player.setAttribute("low-power-idle", "");
1291
+ player.toggleAttribute("no-load-timeout", noLoadTimeout === true);
1289
1292
  player.setAttribute("width", String(portrait ? 1080 : 1920));
1290
1293
  player.setAttribute("height", String(portrait ? 1920 : 1080));
1291
1294
  player.style.width = "100%";
package/dist/index.html CHANGED
@@ -14,7 +14,7 @@
14
14
  document.documentElement.dataset.theme = "paper";
15
15
  }
16
16
  </script>
17
- <script type="module" crossorigin src="/assets/index-CV00F8qG.js"></script>
17
+ <script type="module" crossorigin src="/assets/index-CVxB8lqm.js"></script>
18
18
  <link rel="stylesheet" crossorigin href="/assets/index-DkQKsFxx.css">
19
19
  </head>
20
20
  <body>
package/dist/index.js CHANGED
@@ -47,7 +47,7 @@ import {
47
47
  usePreviewVariablesStore,
48
48
  whenPreviewBooted,
49
49
  writeStudioUiPreferences
50
- } from "./chunk-4UHZLLQL.js";
50
+ } from "./chunk-BLXESG6S.js";
51
51
  import {
52
52
  Button,
53
53
  ContextMenu,
@@ -30154,7 +30154,8 @@ function useTimelineSyncCallbacks({
30154
30154
  applyPreviewAudioState,
30155
30155
  onAdapterReady = revealAndCommit,
30156
30156
  isCurrent,
30157
- onLoadGiveUp
30157
+ onLoadGiveUp,
30158
+ hostOwnsDeadline
30158
30159
  }) {
30159
30160
  const processTimelineMessage = useCallback38(
30160
30161
  (data) => {
@@ -30291,6 +30292,7 @@ function useTimelineSyncCallbacks({
30291
30292
  };
30292
30293
  window.addEventListener("message", onMessage);
30293
30294
  stopWaitingRef.current = stopWaiting;
30295
+ if (hostOwnsDeadline) return;
30294
30296
  probeIntervalRef.current = setTimeout(() => {
30295
30297
  if (!settled) {
30296
30298
  trySettle();
@@ -30300,7 +30302,14 @@ function useTimelineSyncCallbacks({
30300
30302
  revealIframe(iframeRef.current);
30301
30303
  }, 5e3);
30302
30304
  },
30303
- [initializeAdapter, iframeRef, probeIntervalRef, applyPreviewAudioState, onLoadGiveUp]
30305
+ [
30306
+ initializeAdapter,
30307
+ iframeRef,
30308
+ probeIntervalRef,
30309
+ applyPreviewAudioState,
30310
+ onLoadGiveUp,
30311
+ hostOwnsDeadline
30312
+ ]
30304
30313
  );
30305
30314
  const cancelPendingLoad = useCallback38(() => {
30306
30315
  if (probeIntervalRef.current) clearInterval(probeIntervalRef.current);
@@ -31697,7 +31706,8 @@ function useShadowPreviewReload({
31697
31706
  applyPreviewAudioState: () => {
31698
31707
  },
31699
31708
  onAdapterReady: markAdapterReady,
31700
- isCurrent: isCurrentShadow
31709
+ isCurrent: isCurrentShadow,
31710
+ hostOwnsDeadline: true
31701
31711
  });
31702
31712
  cancelPendingLoadRef.current = cancelPendingLoad;
31703
31713
  const onShadowReadyChange = useCallback39(
@@ -33928,6 +33938,7 @@ var NLEPreview = memo23(function NLEPreview2({
33928
33938
  onPreviewError: (message) => onShadowError(slot.gen, message),
33929
33939
  portrait,
33930
33940
  suppressLoadingOverlay: true,
33941
+ noLoadTimeout: true,
33931
33942
  style: SHADOW_IFRAME_STYLE
33932
33943
  },
33933
33944
  `${activeKey}-${slot.gen}`
package/dist/player.d.ts CHANGED
@@ -16,6 +16,7 @@ interface PlayerProps {
16
16
  portrait?: boolean;
17
17
  style?: React.CSSProperties;
18
18
  suppressLoadingOverlay?: boolean;
19
+ noLoadTimeout?: boolean;
19
20
  }
20
21
  /**
21
22
  * Renders a composition preview using the <hyperframes-player> web component.
package/dist/player.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  Player
3
- } from "./chunk-4UHZLLQL.js";
3
+ } from "./chunk-BLXESG6S.js";
4
4
  import "./chunk-FF5DM7BQ.js";
5
5
  import "./chunk-T43DM7Q6.js";
6
6
  import "./chunk-MLKGABMK.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperframes/studio",
3
- "version": "0.8.170",
3
+ "version": "0.8.172",
4
4
  "description": "",
5
5
  "repository": {
6
6
  "type": "git",
@@ -59,12 +59,12 @@
59
59
  "dockview-react": "8.3.1",
60
60
  "gsap": "^3.13.0",
61
61
  "mediabunny": "^1.45.3",
62
- "@hyperframes/core": "0.8.170",
63
- "@hyperframes/parsers": "0.8.170",
64
- "@hyperframes/shader-transitions": "0.8.170",
65
- "@hyperframes/player": "0.8.170",
66
- "@hyperframes/studio-server": "0.8.170",
67
- "@hyperframes/sdk": "0.8.170"
62
+ "@hyperframes/player": "0.8.172",
63
+ "@hyperframes/sdk": "0.8.172",
64
+ "@hyperframes/shader-transitions": "0.8.172",
65
+ "@hyperframes/core": "0.8.172",
66
+ "@hyperframes/studio-server": "0.8.172",
67
+ "@hyperframes/parsers": "0.8.172"
68
68
  },
69
69
  "devDependencies": {
70
70
  "@tailwindcss/vite": "^4.3.3",
@@ -81,7 +81,7 @@
81
81
  "vite": "^6.4.2",
82
82
  "vitest": "^4.1.11",
83
83
  "zustand": "^5.0.0",
84
- "@hyperframes/producer": "0.8.170"
84
+ "@hyperframes/producer": "0.8.172"
85
85
  },
86
86
  "peerDependencies": {
87
87
  "react": "19",
@@ -0,0 +1,63 @@
1
+ // @vitest-environment happy-dom
2
+ // @vitest-environment-options {"happyDOM":{"settings":{"disableIframePageLoading":true}}}
3
+
4
+ import React, { act, createRef } from "react";
5
+ import { createRoot } from "react-dom/client";
6
+ import { afterEach, expect, it, vi } from "vitest";
7
+ import { NLEPreview } from "./NLEPreview";
8
+
9
+ globalThis.IS_REACT_ACT_ENVIRONMENT = true;
10
+
11
+ afterEach(() => {
12
+ vi.useRealTimers();
13
+ document.body.innerHTML = "";
14
+ });
15
+
16
+ it("keeps a shadow reload whose runtime boots after 8 s, and its player comes ready", async () => {
17
+ await import("@hyperframes/player");
18
+ vi.useFakeTimers();
19
+ const onShadowError = vi.fn();
20
+ const host = document.createElement("div");
21
+ document.body.append(host);
22
+ const root = createRoot(host);
23
+ await act(async () => {
24
+ root.render(
25
+ React.createElement(NLEPreview, {
26
+ projectId: "p",
27
+ iframeRef: createRef<HTMLIFrameElement>(),
28
+ onIframeLoad: () => {},
29
+ previewSlots: [
30
+ { gen: 0, role: "live" },
31
+ { gen: 1, role: "shadow", url: "/api/projects/p/preview?_t=1" },
32
+ ],
33
+ onShadowIframeLoad: () => {},
34
+ onShadowReadyChange: () => {},
35
+ onShadowError,
36
+ setShadowIframeNode: () => {},
37
+ resetPreviewSlots: () => {},
38
+ }),
39
+ );
40
+ });
41
+
42
+ const shadow = host.querySelectorAll<
43
+ HTMLElement & { iframeElement: HTMLIFrameElement; ready: boolean }
44
+ >("hyperframes-player")[1];
45
+ const iframe = shadow.iframeElement;
46
+ act(() => {
47
+ iframe.dispatchEvent(new Event("load"));
48
+ });
49
+ act(() => {
50
+ vi.advanceTimersByTime(10_000);
51
+ });
52
+ expect(onShadowError).not.toHaveBeenCalled();
53
+
54
+ Object.assign(iframe.contentWindow!, { __player: { getDuration: () => 9 } });
55
+ act(() => {
56
+ vi.advanceTimersByTime(1_000);
57
+ });
58
+ expect(onShadowError).not.toHaveBeenCalled();
59
+ expect(shadow.ready).toBe(true);
60
+ // The live slot keeps the player's own 8 s deadline.
61
+ expect(host.querySelector("hyperframes-player")?.hasAttribute("no-load-timeout")).toBe(false);
62
+ act(() => root.unmount());
63
+ });
@@ -209,8 +209,7 @@ export const NLEPreview = memo(function NLEPreview({
209
209
 
210
210
  const updateCompositionSizeFromPreview = useCallback(() => {
211
211
  const next = readPreviewCompositionSize(previewIframeRef.current);
212
- // Pure updater — the parent notification happens in the effect below
213
- // (updaters may run more than once under Strict Mode / concurrent React).
212
+ // Pure updater; the effect below notifies the parent (Strict Mode may run updaters twice).
214
213
  setCompositionSize((prev) =>
215
214
  prev?.width === next?.width && prev?.height === next?.height ? prev : next,
216
215
  );
@@ -563,6 +562,7 @@ export const NLEPreview = memo(function NLEPreview({
563
562
  onPreviewError={(message) => onShadowError(slot.gen, message)}
564
563
  portrait={portrait}
565
564
  suppressLoadingOverlay
565
+ noLoadTimeout
566
566
  style={SHADOW_IFRAME_STYLE}
567
567
  />
568
568
  ),
@@ -37,6 +37,7 @@ interface PlayerProps {
37
37
  portrait?: boolean;
38
38
  style?: React.CSSProperties;
39
39
  suppressLoadingOverlay?: boolean;
40
+ noLoadTimeout?: boolean;
40
41
  }
41
42
 
42
43
  interface HyperframesPlayerElement extends HTMLElement {
@@ -154,6 +155,7 @@ export const Player = forwardRef<HTMLIFrameElement, PlayerProps>(
154
155
  portrait,
155
156
  style,
156
157
  suppressLoadingOverlay,
158
+ noLoadTimeout,
157
159
  },
158
160
  ref,
159
161
  ) => {
@@ -224,6 +226,7 @@ export const Player = forwardRef<HTMLIFrameElement, PlayerProps>(
224
226
  setCompositionLoading(true);
225
227
  loadStartedAtRef.current = performance.now();
226
228
  loadIdRef.current += 1;
229
+ player.removeAttribute("no-load-timeout");
227
230
  player.setAttribute("src", retryUrl.pathname + retryUrl.search);
228
231
  };
229
232
  retryPreviewRef.current = retryPreview;
@@ -331,6 +334,7 @@ export const Player = forwardRef<HTMLIFrameElement, PlayerProps>(
331
334
  player.setAttribute("shader-capture-scale", "1");
332
335
  player.setAttribute("shader-loading", "player");
333
336
  player.setAttribute("low-power-idle", "");
337
+ player.toggleAttribute("no-load-timeout", noLoadTimeout === true);
334
338
  player.setAttribute("width", String(portrait ? 1080 : 1920));
335
339
  player.setAttribute("height", String(portrait ? 1920 : 1080));
336
340
  player.style.width = "100%";
@@ -49,7 +49,12 @@ function isDocumentHidden(): boolean {
49
49
 
50
50
  type UseShadowPreviewReloadParams = Omit<
51
51
  UseTimelineSyncCallbacksParams,
52
- "probeIntervalRef" | "onAdapterReady" | "getAdapter" | "isCurrent" | "onLoadGiveUp"
52
+ | "probeIntervalRef"
53
+ | "onAdapterReady"
54
+ | "getAdapter"
55
+ | "isCurrent"
56
+ | "onLoadGiveUp"
57
+ | "hostOwnsDeadline"
53
58
  > & {
54
59
  getAdapter: (overrideIframe?: HTMLIFrameElement | null) => PlaybackAdapter | null;
55
60
  /** Runs right after a shadow becomes the live iframe (iframeRef already points at it). */
@@ -249,6 +254,7 @@ export function useShadowPreviewReload({
249
254
  applyPreviewAudioState: () => {},
250
255
  onAdapterReady: markAdapterReady,
251
256
  isCurrent: isCurrentShadow,
257
+ hostOwnsDeadline: true,
252
258
  });
253
259
  cancelPendingLoadRef.current = cancelPendingLoad;
254
260
 
@@ -461,6 +461,31 @@ describe("shadow reload readiness and failure", () => {
461
461
  unmount(root);
462
462
  });
463
463
 
464
+ it("promotes a shadow whose runtime boots after Studio's own 5 s readiness wait", () => {
465
+ vi.useFakeTimers();
466
+ const onPreviewReloadFailed = vi.fn();
467
+ const { getApi, gen, root } = beginReload({ onPreviewReloadFailed });
468
+ const { win } = makeAdapterWindow();
469
+ const booting: Record<string, unknown> = { ...win, __player: undefined };
470
+ const iframe = makeFakeIframe(booting);
471
+ act(() => {
472
+ getApi().setShadowIframeNode(iframe);
473
+ getApi().onShadowIframeLoad(gen);
474
+ });
475
+
476
+ act(() => void vi.advanceTimersByTime(10_000));
477
+ booting.__player = win.__player;
478
+ act(() => {
479
+ window.dispatchEvent(
480
+ new MessageEvent("message", { data: { source: "hf-preview", type: "state" } }),
481
+ );
482
+ getApi().onShadowReadyChange(gen, true);
483
+ });
484
+ expect(getApi().iframeRef.current).toBe(iframe);
485
+ expect(onPreviewReloadFailed).not.toHaveBeenCalled();
486
+ unmount(root);
487
+ });
488
+
464
489
  it("gives a reload that replaces a slow one its own full set of budgets", () => {
465
490
  vi.useFakeTimers();
466
491
  vi.spyOn(console, "error").mockImplementation(() => {});
@@ -60,6 +60,7 @@ export interface UseTimelineSyncCallbacksParams {
60
60
  isCurrent?: (context?: number) => boolean;
61
61
  /** Fires when no adapter appeared within the wait window. */
62
62
  onLoadGiveUp?: (context?: number) => void;
63
+ hostOwnsDeadline?: boolean;
63
64
  }
64
65
 
65
66
  /**
@@ -163,6 +164,7 @@ export function useTimelineSyncCallbacks({
163
164
  onAdapterReady = revealAndCommit,
164
165
  isCurrent,
165
166
  onLoadGiveUp,
167
+ hostOwnsDeadline,
166
168
  }: UseTimelineSyncCallbacksParams) {
167
169
  // Convert a runtime timeline message (from iframe postMessage) into TimelineElements
168
170
  const processTimelineMessage = useCallback(
@@ -335,6 +337,7 @@ export function useTimelineSyncCallbacks({
335
337
  };
336
338
  window.addEventListener("message", onMessage);
337
339
  stopWaitingRef.current = stopWaiting;
340
+ if (hostOwnsDeadline) return;
338
341
 
339
342
  // Safety net: if no message arrives within 5s, try one last time then give up.
340
343
  probeIntervalRef.current = setTimeout(() => {
@@ -346,7 +349,14 @@ export function useTimelineSyncCallbacks({
346
349
  revealIframe(iframeRef.current);
347
350
  }, 5000) as unknown as ReturnType<typeof setInterval>;
348
351
  },
349
- [initializeAdapter, iframeRef, probeIntervalRef, applyPreviewAudioState, onLoadGiveUp],
352
+ [
353
+ initializeAdapter,
354
+ iframeRef,
355
+ probeIntervalRef,
356
+ applyPreviewAudioState,
357
+ onLoadGiveUp,
358
+ hostOwnsDeadline,
359
+ ],
350
360
  );
351
361
 
352
362
  const cancelPendingLoad = useCallback(() => {