mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
feat(runtime): swallow() helper replaces empty catches in inlined runtime
After hf#641 inlined the runtime IIFE into every bundle, lint tools
inspecting bundled output (including Abhay's c2v eval) started flagging
empty `catch {}` blocks across the runtime. The source had explanatory
comments inside, but esbuild's minifier strips them — the IIFE ships
~10 visible patterns of `}catch{}` and consumers' linters fire on each.
Each empty catch is intentional best-effort error swallowing —
postMessage to a parent frame that may not exist, `media.play()` /
`pause()` that throw under autoplay restrictions, timeline `seek()` on
a disposed timeline, anime.js / lottie feature detection on hosts that
don't load those libraries, etc. The right behaviour stays "tried,
didn't work, move on", but doing it visibly improves three things:
- lint clean: helper call is a real statement; no `no-empty` warnings
survive minification
- debuggable: flip `window.__hfDebug = true` in DevTools to see every
swallow site with `console.debug` (silent in prod by default)
- observable: studio / embeddings can install
`window.__hf.onSwallowed = handler` to collect runtime swallow
events without polluting the page console
Implementation: `packages/core/src/runtime/diagnostics.ts` exports
`swallow(label, err?)`. 41 catch sites across 12 runtime files
converted via mechanical pass (auto-generated `runtime.<module>.siteN`
labels — labels can be tightened site-by-site as a follow-up; the
shape of the change is what matters here).
Verification:
- core 674/674 (incl. 6 new diagnostics tests covering silent default,
__hfDebug logging, legacy __HYPERFRAMES_DEBUG flag, handler hook,
handler-throws-doesn't-recurse, both-active)
- typecheck clean
- format / lint clean
- runtime IIFE rebuilds successfully (`bun run build:hyperframes-runtime`)
Refs Abhay's c2v eval — bundler artefacts now lint-clean with the
runtime body inlined.
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import type { RuntimeDeterministicAdapter } from "../types";
|
import type { RuntimeDeterministicAdapter } from "../types";
|
||||||
|
import { swallow } from "../diagnostics";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* anime.js adapter for HyperFrames
|
* anime.js adapter for HyperFrames
|
||||||
@@ -61,8 +62,9 @@ export function createAnimeJsAdapter(): RuntimeDeterministicAdapter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
(window as AnimeWindow).__hfAnime = existing;
|
(window as AnimeWindow).__hfAnime = existing;
|
||||||
} catch {
|
} catch (err) {
|
||||||
// ignore discovery failures
|
// ignore discovery failures
|
||||||
|
swallow("runtime.adapters.animejs.site1", err);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -76,8 +78,9 @@ export function createAnimeJsAdapter(): RuntimeDeterministicAdapter {
|
|||||||
if (typeof instance.seek === "function") {
|
if (typeof instance.seek === "function") {
|
||||||
instance.seek(timeMs);
|
instance.seek(timeMs);
|
||||||
}
|
}
|
||||||
} catch {
|
} catch (err) {
|
||||||
// ignore per-instance failures — keep going for other instances
|
// ignore per-instance failures — keep going for other instances
|
||||||
|
swallow("runtime.adapters.animejs.site2", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -91,8 +94,9 @@ export function createAnimeJsAdapter(): RuntimeDeterministicAdapter {
|
|||||||
if (typeof instance.pause === "function") {
|
if (typeof instance.pause === "function") {
|
||||||
instance.pause();
|
instance.pause();
|
||||||
}
|
}
|
||||||
} catch {
|
} catch (err) {
|
||||||
// ignore
|
// ignore
|
||||||
|
swallow("runtime.adapters.animejs.site3", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -106,8 +110,9 @@ export function createAnimeJsAdapter(): RuntimeDeterministicAdapter {
|
|||||||
if (typeof instance.play === "function") {
|
if (typeof instance.play === "function") {
|
||||||
instance.play();
|
instance.play();
|
||||||
}
|
}
|
||||||
} catch {
|
} catch (err) {
|
||||||
// ignore
|
// ignore
|
||||||
|
swallow("runtime.adapters.animejs.site4", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { RuntimeDeterministicAdapter } from "../types";
|
import type { RuntimeDeterministicAdapter } from "../types";
|
||||||
|
import { swallow } from "../diagnostics";
|
||||||
|
|
||||||
export function createCssAdapter(params?: {
|
export function createCssAdapter(params?: {
|
||||||
resolveStartSeconds?: (element: Element) => number;
|
resolveStartSeconds?: (element: Element) => number;
|
||||||
@@ -22,13 +23,15 @@ export function createCssAdapter(params?: {
|
|||||||
for (const animation of animations) {
|
for (const animation of animations) {
|
||||||
try {
|
try {
|
||||||
animation.currentTime = timeMs;
|
animation.currentTime = timeMs;
|
||||||
} catch {
|
} catch (err) {
|
||||||
// ignore animations that reject currentTime writes
|
// ignore animations that reject currentTime writes
|
||||||
|
swallow("runtime.adapters.css.site1", err);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
animation.pause();
|
animation.pause();
|
||||||
} catch {
|
} catch (err) {
|
||||||
// infinite unresolved animations can throw on pause before currentTime sticks
|
// infinite unresolved animations can throw on pause before currentTime sticks
|
||||||
|
swallow("runtime.adapters.css.site2", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -37,8 +40,9 @@ export function createCssAdapter(params?: {
|
|||||||
for (const animation of animations) {
|
for (const animation of animations) {
|
||||||
try {
|
try {
|
||||||
animation.play();
|
animation.play();
|
||||||
} catch {
|
} catch (err) {
|
||||||
// ignore animation edge-cases
|
// ignore animation edge-cases
|
||||||
|
swallow("runtime.adapters.css.site3", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -47,8 +51,9 @@ export function createCssAdapter(params?: {
|
|||||||
for (const animation of animations) {
|
for (const animation of animations) {
|
||||||
try {
|
try {
|
||||||
animation.pause();
|
animation.pause();
|
||||||
} catch {
|
} catch (err) {
|
||||||
// ignore animation edge-cases
|
// ignore animation edge-cases
|
||||||
|
swallow("runtime.adapters.css.site4", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { RuntimeDeterministicAdapter } from "../types";
|
import type { RuntimeDeterministicAdapter } from "../types";
|
||||||
|
import { swallow } from "../diagnostics";
|
||||||
export { isLottieAnimationLoaded } from "./lottieReadiness";
|
export { isLottieAnimationLoaded } from "./lottieReadiness";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -71,8 +72,9 @@ export function createLottieAdapter(): RuntimeDeterministicAdapter {
|
|||||||
(window as LottieWindow).__hfLottie = existing;
|
(window as LottieWindow).__hfLottie = existing;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} catch (err) {
|
||||||
// ignore discovery failures
|
// ignore discovery failures
|
||||||
|
swallow("runtime.adapters.lottie.site1", err);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -107,8 +109,9 @@ export function createLottieAdapter(): RuntimeDeterministicAdapter {
|
|||||||
anim.seek(percentage);
|
anim.seek(percentage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} catch (err) {
|
||||||
// ignore per-animation failures — keep going for other instances
|
// ignore per-animation failures — keep going for other instances
|
||||||
|
swallow("runtime.adapters.lottie.site2", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -124,8 +127,9 @@ export function createLottieAdapter(): RuntimeDeterministicAdapter {
|
|||||||
} else if (isDotLottiePlayer(anim)) {
|
} else if (isDotLottiePlayer(anim)) {
|
||||||
anim.pause();
|
anim.pause();
|
||||||
}
|
}
|
||||||
} catch {
|
} catch (err) {
|
||||||
// ignore
|
// ignore
|
||||||
|
swallow("runtime.adapters.lottie.site3", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { RuntimeDeterministicAdapter } from "../types";
|
import type { RuntimeDeterministicAdapter } from "../types";
|
||||||
|
import { swallow } from "../diagnostics";
|
||||||
|
|
||||||
export function createThreeAdapter(): RuntimeDeterministicAdapter {
|
export function createThreeAdapter(): RuntimeDeterministicAdapter {
|
||||||
let forcedTime: number | null = null;
|
let forcedTime: number | null = null;
|
||||||
@@ -13,8 +14,9 @@ export function createThreeAdapter(): RuntimeDeterministicAdapter {
|
|||||||
window.__hfThreeTime = forcedTime;
|
window.__hfThreeTime = forcedTime;
|
||||||
try {
|
try {
|
||||||
window.dispatchEvent(new CustomEvent("hf-seek", { detail: { time: forcedTime } }));
|
window.dispatchEvent(new CustomEvent("hf-seek", { detail: { time: forcedTime } }));
|
||||||
} catch {
|
} catch (err) {
|
||||||
// ignore custom event failures
|
// ignore custom event failures
|
||||||
|
swallow("runtime.adapters.three.site1", err);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
pause: () => {
|
pause: () => {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { RuntimeDeterministicAdapter } from "../types";
|
import type { RuntimeDeterministicAdapter } from "../types";
|
||||||
|
import { swallow } from "../diagnostics";
|
||||||
|
|
||||||
export function createWaapiAdapter(): RuntimeDeterministicAdapter {
|
export function createWaapiAdapter(): RuntimeDeterministicAdapter {
|
||||||
return {
|
return {
|
||||||
@@ -10,13 +11,15 @@ export function createWaapiAdapter(): RuntimeDeterministicAdapter {
|
|||||||
for (const animation of document.getAnimations()) {
|
for (const animation of document.getAnimations()) {
|
||||||
try {
|
try {
|
||||||
animation.currentTime = timeMs;
|
animation.currentTime = timeMs;
|
||||||
} catch {
|
} catch (err) {
|
||||||
// ignore animations that reject currentTime writes
|
// ignore animations that reject currentTime writes
|
||||||
|
swallow("runtime.adapters.waapi.site1", err);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
animation.pause();
|
animation.pause();
|
||||||
} catch {
|
} catch (err) {
|
||||||
// infinite unresolved animations can throw here until currentTime resolves
|
// infinite unresolved animations can throw here until currentTime resolves
|
||||||
|
swallow("runtime.adapters.waapi.site2", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -25,8 +28,9 @@ export function createWaapiAdapter(): RuntimeDeterministicAdapter {
|
|||||||
for (const animation of document.getAnimations()) {
|
for (const animation of document.getAnimations()) {
|
||||||
try {
|
try {
|
||||||
animation.pause();
|
animation.pause();
|
||||||
} catch {
|
} catch (err) {
|
||||||
// ignore animation edge-cases
|
// ignore animation edge-cases
|
||||||
|
swallow("runtime.adapters.waapi.site3", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { swallow } from "./diagnostics";
|
||||||
/**
|
/**
|
||||||
* Runtime analytics & performance telemetry — vendor-agnostic event emission.
|
* Runtime analytics & performance telemetry — vendor-agnostic event emission.
|
||||||
*
|
*
|
||||||
@@ -75,8 +76,9 @@ export function emitAnalyticsEvent(
|
|||||||
event,
|
event,
|
||||||
properties: properties ?? {},
|
properties: properties ?? {},
|
||||||
});
|
});
|
||||||
} catch {
|
} catch (err) {
|
||||||
// Never let analytics failures affect the runtime
|
// Never let analytics failures affect the runtime
|
||||||
|
swallow("runtime.analytics.site1", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,8 +109,9 @@ export function emitPerformanceMetric(
|
|||||||
if (typeof performance !== "undefined" && typeof performance.mark === "function") {
|
if (typeof performance !== "undefined" && typeof performance.mark === "function") {
|
||||||
performance.mark(name, { detail: { value, tags: tags ?? {} } });
|
performance.mark(name, { detail: { value, tags: tags ?? {} } });
|
||||||
}
|
}
|
||||||
} catch {
|
} catch (err) {
|
||||||
// performance API unavailable or rejected — keep going
|
// performance API unavailable or rejected — keep going
|
||||||
|
swallow("runtime.analytics.site2", err);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_postMessage) return;
|
if (!_postMessage) return;
|
||||||
@@ -120,7 +123,8 @@ export function emitPerformanceMetric(
|
|||||||
value,
|
value,
|
||||||
tags: tags ?? {},
|
tags: tags ?? {},
|
||||||
});
|
});
|
||||||
} catch {
|
} catch (err) {
|
||||||
// Never let telemetry failures affect the runtime
|
// Never let telemetry failures affect the runtime
|
||||||
|
swallow("runtime.analytics.site3", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { swallow } from "./diagnostics";
|
||||||
import type { RuntimeBridgeControlMessage, RuntimeOutboundMessage } from "./types";
|
import type { RuntimeBridgeControlMessage, RuntimeOutboundMessage } from "./types";
|
||||||
|
|
||||||
type BridgeDeps = {
|
type BridgeDeps = {
|
||||||
@@ -15,8 +16,9 @@ type BridgeDeps = {
|
|||||||
export function postRuntimeMessage(payload: RuntimeOutboundMessage): void {
|
export function postRuntimeMessage(payload: RuntimeOutboundMessage): void {
|
||||||
try {
|
try {
|
||||||
window.parent.postMessage(payload, "*");
|
window.parent.postMessage(payload, "*");
|
||||||
} catch {
|
} catch (err) {
|
||||||
// Ignore cross-frame posting failures.
|
// Cross-frame posting can throw if the parent is gone or origin-isolated.
|
||||||
|
swallow("bridge.postMessage", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,8 +106,9 @@ function flashElements(selectors: string[], duration: number): void {
|
|||||||
el.classList.add("__hf-flash");
|
el.classList.add("__hf-flash");
|
||||||
setTimeout(() => el.classList.remove("__hf-flash"), duration);
|
setTimeout(() => el.classList.remove("__hf-flash"), duration);
|
||||||
});
|
});
|
||||||
} catch {
|
} catch (err) {
|
||||||
// Invalid selector — skip
|
// Invalid selector — skip
|
||||||
|
swallow("bridge.flashElements.querySelector", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
// @vitest-environment happy-dom
|
||||||
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import { swallow } from "./diagnostics";
|
||||||
|
|
||||||
|
interface HFTestWindow {
|
||||||
|
__hfDebug?: boolean;
|
||||||
|
__HYPERFRAMES_DEBUG?: boolean;
|
||||||
|
__hf?: {
|
||||||
|
onSwallowed?: (e: { label: string; error: unknown }) => void;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("swallow", () => {
|
||||||
|
const w = window as unknown as HFTestWindow;
|
||||||
|
const originalDebug = console.debug;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
delete w.__hfDebug;
|
||||||
|
delete w.__HYPERFRAMES_DEBUG;
|
||||||
|
delete w.__hf;
|
||||||
|
console.debug = vi.fn();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
console.debug = originalDebug;
|
||||||
|
delete w.__hfDebug;
|
||||||
|
delete w.__HYPERFRAMES_DEBUG;
|
||||||
|
delete w.__hf;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("is silent by default — no console output, no handler call", () => {
|
||||||
|
swallow("test.silent", new Error("boom"));
|
||||||
|
expect(console.debug).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("logs to console.debug when window.__hfDebug is true", () => {
|
||||||
|
w.__hfDebug = true;
|
||||||
|
const err = new Error("boom");
|
||||||
|
swallow("test.debug", err);
|
||||||
|
expect(console.debug).toHaveBeenCalledWith("[hyperframes] test.debug swallowed:", err);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("also honors window.__HYPERFRAMES_DEBUG (legacy flag)", () => {
|
||||||
|
w.__HYPERFRAMES_DEBUG = true;
|
||||||
|
swallow("test.legacy", "string-error");
|
||||||
|
expect(console.debug).toHaveBeenCalledWith(
|
||||||
|
"[hyperframes] test.legacy swallowed:",
|
||||||
|
"string-error",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("dispatches to window.__hf.onSwallowed when installed", () => {
|
||||||
|
const handler = vi.fn();
|
||||||
|
w.__hf = { onSwallowed: handler };
|
||||||
|
const err = new Error("from handler");
|
||||||
|
swallow("test.handler", err);
|
||||||
|
expect(handler).toHaveBeenCalledWith({ label: "test.handler", error: err });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not propagate errors from the user-installed handler", () => {
|
||||||
|
w.__hf = {
|
||||||
|
onSwallowed: () => {
|
||||||
|
throw new Error("handler exploded");
|
||||||
|
},
|
||||||
|
};
|
||||||
|
expect(() => swallow("test.handler-throws", new Error("real"))).not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("can run with both handler AND debug flag set", () => {
|
||||||
|
w.__hfDebug = true;
|
||||||
|
const handler = vi.fn();
|
||||||
|
w.__hf = { onSwallowed: handler };
|
||||||
|
swallow("test.both", "err");
|
||||||
|
expect(handler).toHaveBeenCalled();
|
||||||
|
expect(console.debug).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
/**
|
||||||
|
* Runtime diagnostic helpers for best-effort operations.
|
||||||
|
*
|
||||||
|
* Many runtime operations (postMessage to a parent frame, `media.play()` /
|
||||||
|
* `pause()` / `currentTime=`, timeline `seek()`, anime.js feature detection,
|
||||||
|
* etc.) can throw under perfectly normal conditions: the parent frame is
|
||||||
|
* cross-origin, autoplay is denied, the media element was just removed from
|
||||||
|
* the DOM, the timeline has been disposed, the host page does not include
|
||||||
|
* anime.js. The right behaviour in each case is "tried, didn't work, move
|
||||||
|
* on" — but emitting nothing makes silent failures invisible to anyone
|
||||||
|
* debugging a genuinely broken composition, and the bare `catch {}` shape
|
||||||
|
* also trips strict lint configurations on the inlined runtime IIFE.
|
||||||
|
*
|
||||||
|
* `swallow(label, err)` is the single funnel for these intentional silences.
|
||||||
|
* It dispatches to:
|
||||||
|
*
|
||||||
|
* - `console.debug` with the label, the error, and a `[hyperframes]` prefix
|
||||||
|
* when `window.__hfDebug === true` (or the legacy `__HYPERFRAMES_DEBUG`
|
||||||
|
* env-style global). Quiet by default; flip the flag in DevTools when
|
||||||
|
* hunting a regression.
|
||||||
|
* - A custom `__hf.onSwallowed` handler if installed — lets the studio /
|
||||||
|
* embeddings collect runtime swallow events without polluting the page
|
||||||
|
* console.
|
||||||
|
*
|
||||||
|
* Production behaviour without either flag set: completely silent, just
|
||||||
|
* like the original empty `catch {}`. The shape is also lint-clean — the
|
||||||
|
* helper call is a real statement, so no `no-empty` warnings ship in the
|
||||||
|
* inlined IIFE.
|
||||||
|
*/
|
||||||
|
export interface SwallowedEvent {
|
||||||
|
/** Short, descriptive label naming the operation that failed. */
|
||||||
|
label: string;
|
||||||
|
/** The thrown value (often an Error, but JS allows anything). */
|
||||||
|
error: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface HFDebugSurface {
|
||||||
|
__hfDebug?: boolean;
|
||||||
|
__HYPERFRAMES_DEBUG?: boolean;
|
||||||
|
__hf?: {
|
||||||
|
onSwallowed?: (event: SwallowedEvent) => void;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function swallow(label: string, error?: unknown): void {
|
||||||
|
if (typeof window === "undefined") return;
|
||||||
|
const w = window as unknown as HFDebugSurface;
|
||||||
|
|
||||||
|
const handler = w.__hf?.onSwallowed;
|
||||||
|
if (handler) {
|
||||||
|
try {
|
||||||
|
handler({ label, error });
|
||||||
|
} catch (handlerError) {
|
||||||
|
// Don't recurse into swallow() — a consumer hook that throws
|
||||||
|
// shouldn't be allowed to take down the runtime, and routing the
|
||||||
|
// failure back through swallow() would loop. Drop on the floor;
|
||||||
|
// the original error already had its surface above.
|
||||||
|
void handlerError;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (w.__hfDebug || w.__HYPERFRAMES_DEBUG) {
|
||||||
|
// eslint-disable-next-line no-console -- intentional debug surface
|
||||||
|
console.debug(`[hyperframes] ${label} swallowed:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@ import { loadExternalCompositions, loadInlineTemplateCompositions } from "./comp
|
|||||||
import { applyCaptionOverrides } from "./captionOverrides";
|
import { applyCaptionOverrides } from "./captionOverrides";
|
||||||
import type { RuntimeDeterministicAdapter, RuntimeJson, RuntimeTimelineLike } from "./types";
|
import type { RuntimeDeterministicAdapter, RuntimeJson, RuntimeTimelineLike } from "./types";
|
||||||
import type { PlayerAPI } from "../core.types";
|
import type { PlayerAPI } from "../core.types";
|
||||||
|
import { swallow } from "./diagnostics";
|
||||||
|
|
||||||
const AUTHORED_DURATION_ATTR = "data-hf-authored-duration";
|
const AUTHORED_DURATION_ATTR = "data-hf-authored-duration";
|
||||||
const AUTHORED_END_ATTR = "data-hf-authored-end";
|
const AUTHORED_END_ATTR = "data-hf-authored-end";
|
||||||
@@ -33,8 +34,9 @@ export function initSandboxRuntimeModular(): void {
|
|||||||
if (typeof runtimeWindow.__hfRuntimeTeardown === "function") {
|
if (typeof runtimeWindow.__hfRuntimeTeardown === "function") {
|
||||||
try {
|
try {
|
||||||
runtimeWindow.__hfRuntimeTeardown();
|
runtimeWindow.__hfRuntimeTeardown();
|
||||||
} catch {
|
} catch (err) {
|
||||||
// keep runtime resilient across reinits
|
// keep runtime resilient across reinits
|
||||||
|
swallow("runtime.init.site1", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Normalize html/body so browser defaults (8px margin, white background) never
|
// Normalize html/body so browser defaults (8px margin, white background) never
|
||||||
@@ -583,8 +585,9 @@ export function initSandboxRuntimeModular(): void {
|
|||||||
if (existingRootTimeline) {
|
if (existingRootTimeline) {
|
||||||
try {
|
try {
|
||||||
fallbackTimeline.add(existingRootTimeline, 0);
|
fallbackTimeline.add(existingRootTimeline, 0);
|
||||||
} catch {
|
} catch (err) {
|
||||||
// keep fallback resilient if root add fails
|
// keep fallback resilient if root add fails
|
||||||
|
swallow("runtime.init.site2", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const withTween = fallbackTimeline as RuntimeTimelineLike & {
|
const withTween = fallbackTimeline as RuntimeTimelineLike & {
|
||||||
@@ -593,8 +596,9 @@ export function initSandboxRuntimeModular(): void {
|
|||||||
if (typeof withTween.to === "function") {
|
if (typeof withTween.to === "function") {
|
||||||
try {
|
try {
|
||||||
withTween.to({}, { duration: durationSeconds });
|
withTween.to({}, { duration: durationSeconds });
|
||||||
} catch {
|
} catch (err) {
|
||||||
// no-op; if tween creation fails, caller will discard by unusable duration
|
// no-op; if tween creation fails, caller will discard by unusable duration
|
||||||
|
swallow("runtime.init.site3", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return fallbackTimeline;
|
return fallbackTimeline;
|
||||||
@@ -622,8 +626,9 @@ export function initSandboxRuntimeModular(): void {
|
|||||||
const startSec = resolveCompositionStartSeconds(candidate.compositionId);
|
const startSec = resolveCompositionStartSeconds(candidate.compositionId);
|
||||||
rootTimeline.add(candidate.timeline, startSec);
|
rootTimeline.add(candidate.timeline, startSec);
|
||||||
addedIds.push(candidate.compositionId);
|
addedIds.push(candidate.compositionId);
|
||||||
} catch {
|
} catch (err) {
|
||||||
// ignore broken child add attempts
|
// ignore broken child add attempts
|
||||||
|
swallow("runtime.init.site4", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return addedIds;
|
return addedIds;
|
||||||
@@ -687,8 +692,9 @@ export function initSandboxRuntimeModular(): void {
|
|||||||
if (typeof timelineWithPaused.paused !== "function") continue;
|
if (typeof timelineWithPaused.paused !== "function") continue;
|
||||||
try {
|
try {
|
||||||
timelineWithPaused.paused(false);
|
timelineWithPaused.paused(false);
|
||||||
} catch {
|
} catch (err) {
|
||||||
// keep runtime resilient against timeline API quirks
|
// keep runtime resilient against timeline API quirks
|
||||||
|
swallow("runtime.init.site5", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -828,8 +834,9 @@ export function initSandboxRuntimeModular(): void {
|
|||||||
// Placing a zero-duration tween at the floor extends
|
// Placing a zero-duration tween at the floor extends
|
||||||
// timeline.duration() to exactly that point.
|
// timeline.duration() to exactly that point.
|
||||||
tlWithTo.to({}, { duration: 0 }, rootDurationFloorSeconds);
|
tlWithTo.to({}, { duration: 0 }, rootDurationFloorSeconds);
|
||||||
} catch {
|
} catch (err) {
|
||||||
// keep runtime resilient
|
// keep runtime resilient
|
||||||
|
swallow("runtime.init.site6", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const newDur = getTimelineDurationSeconds(rootTimeline);
|
const newDur = getTimelineDurationSeconds(rootTimeline);
|
||||||
@@ -1144,8 +1151,9 @@ export function initSandboxRuntimeModular(): void {
|
|||||||
if (wasPlaying) {
|
if (wasPlaying) {
|
||||||
state.capturedTimeline.play();
|
state.capturedTimeline.play();
|
||||||
}
|
}
|
||||||
} catch {
|
} catch (err) {
|
||||||
// keep runtime resilient even if a timeline implementation throws
|
// keep runtime resilient even if a timeline implementation throws
|
||||||
|
swallow("runtime.init.site7", err);
|
||||||
}
|
}
|
||||||
postRuntimeMessage({
|
postRuntimeMessage({
|
||||||
source: "hf-preview",
|
source: "hf-preview",
|
||||||
@@ -1410,14 +1418,16 @@ export function initSandboxRuntimeModular(): void {
|
|||||||
if (method === "discover") adapter.discover();
|
if (method === "discover") adapter.discover();
|
||||||
if (method === "pause") adapter.pause();
|
if (method === "pause") adapter.pause();
|
||||||
if (method === "play" && adapter.play) adapter.play();
|
if (method === "play" && adapter.play) adapter.play();
|
||||||
} catch {
|
} catch (err) {
|
||||||
// keep runtime resilient against adapter-specific failures
|
// keep runtime resilient against adapter-specific failures
|
||||||
|
swallow("runtime.init.site8", err);
|
||||||
}
|
}
|
||||||
if (method === "discover") {
|
if (method === "discover") {
|
||||||
try {
|
try {
|
||||||
adapter.seek({ time: timeSeconds });
|
adapter.seek({ time: timeSeconds });
|
||||||
} catch {
|
} catch (err) {
|
||||||
// ignore seek bootstrap failures
|
// ignore seek bootstrap failures
|
||||||
|
swallow("runtime.init.site9", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1479,8 +1489,9 @@ export function initSandboxRuntimeModular(): void {
|
|||||||
if (!(el instanceof HTMLMediaElement)) continue;
|
if (!(el instanceof HTMLMediaElement)) continue;
|
||||||
try {
|
try {
|
||||||
el.playbackRate = state.playbackRate;
|
el.playbackRate = state.playbackRate;
|
||||||
} catch {
|
} catch (err) {
|
||||||
// ignore unsupported values
|
// ignore unsupported values
|
||||||
|
swallow("runtime.init.site10", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -1509,8 +1520,9 @@ export function initSandboxRuntimeModular(): void {
|
|||||||
for (const adapter of state.deterministicAdapters) {
|
for (const adapter of state.deterministicAdapters) {
|
||||||
try {
|
try {
|
||||||
adapter.seek({ time: Number(timeSeconds) || 0 });
|
adapter.seek({ time: Number(timeSeconds) || 0 });
|
||||||
} catch {
|
} catch (err) {
|
||||||
// ignore adapter failure
|
// ignore adapter failure
|
||||||
|
swallow("runtime.init.site11", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -1739,31 +1751,35 @@ export function initSandboxRuntimeModular(): void {
|
|||||||
if (!adapter || typeof adapter.revert !== "function") continue;
|
if (!adapter || typeof adapter.revert !== "function") continue;
|
||||||
try {
|
try {
|
||||||
adapter.revert();
|
adapter.revert();
|
||||||
} catch {
|
} catch (err) {
|
||||||
// keep runtime resilient against adapter cleanup failures
|
// keep runtime resilient against adapter cleanup failures
|
||||||
|
swallow("runtime.init.site12", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
state.deterministicAdapters = [];
|
state.deterministicAdapters = [];
|
||||||
for (const cleanup of runtimeCleanupCallbacks.splice(0)) {
|
for (const cleanup of runtimeCleanupCallbacks.splice(0)) {
|
||||||
try {
|
try {
|
||||||
cleanup();
|
cleanup();
|
||||||
} catch {
|
} catch (err) {
|
||||||
// ignore cleanup failures
|
// ignore cleanup failures
|
||||||
|
swallow("runtime.init.site13", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (const styleEl of state.injectedCompStyles) {
|
for (const styleEl of state.injectedCompStyles) {
|
||||||
try {
|
try {
|
||||||
styleEl.remove();
|
styleEl.remove();
|
||||||
} catch {
|
} catch (err) {
|
||||||
// ignore cleanup failures
|
// ignore cleanup failures
|
||||||
|
swallow("runtime.init.site14", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
state.injectedCompStyles = [];
|
state.injectedCompStyles = [];
|
||||||
for (const scriptEl of state.injectedCompScripts) {
|
for (const scriptEl of state.injectedCompScripts) {
|
||||||
try {
|
try {
|
||||||
scriptEl.remove();
|
scriptEl.remove();
|
||||||
} catch {
|
} catch (err) {
|
||||||
// ignore cleanup failures
|
// ignore cleanup failures
|
||||||
|
swallow("runtime.init.site15", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
state.injectedCompScripts = [];
|
state.injectedCompScripts = [];
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { swallow } from "./diagnostics";
|
||||||
export type RuntimeMediaClip = {
|
export type RuntimeMediaClip = {
|
||||||
el: HTMLVideoElement | HTMLAudioElement;
|
el: HTMLVideoElement | HTMLAudioElement;
|
||||||
start: number;
|
start: number;
|
||||||
@@ -160,8 +161,9 @@ export function syncRuntimeMedia(params: {
|
|||||||
try {
|
try {
|
||||||
// Per-element rate × global transport rate
|
// Per-element rate × global transport rate
|
||||||
el.playbackRate = clip.playbackRate * params.playbackRate;
|
el.playbackRate = clip.playbackRate * params.playbackRate;
|
||||||
} catch {
|
} catch (err) {
|
||||||
// ignore unsupported playbackRate
|
// ignore unsupported playbackRate
|
||||||
|
swallow("runtime.media.site1", err);
|
||||||
}
|
}
|
||||||
// Drift correction. Forcing `el.currentTime = relTime` every frame
|
// Drift correction. Forcing `el.currentTime = relTime` every frame
|
||||||
// causes an audible seek+rebuffer hiccup (readyState drops briefly).
|
// causes an audible seek+rebuffer hiccup (readyState drops briefly).
|
||||||
@@ -194,8 +196,9 @@ export function syncRuntimeMedia(params: {
|
|||||||
if (drift > 0.5 && (firstTickOfClip || offsetJumped || catastrophicDrift)) {
|
if (drift > 0.5 && (firstTickOfClip || offsetJumped || catastrophicDrift)) {
|
||||||
try {
|
try {
|
||||||
el.currentTime = relTime;
|
el.currentTime = relTime;
|
||||||
} catch {
|
} catch (err) {
|
||||||
// ignore browser seek restrictions
|
// ignore browser seek restrictions
|
||||||
|
swallow("runtime.media.site2", err);
|
||||||
}
|
}
|
||||||
// Detect failed seek: if currentTime didn't reach the target,
|
// Detect failed seek: if currentTime didn't reach the target,
|
||||||
// the browser can't seek past its buffered range. Common with
|
// the browser can't seek past its buffered range. Common with
|
||||||
@@ -208,8 +211,9 @@ export function syncRuntimeMedia(params: {
|
|||||||
el.load();
|
el.load();
|
||||||
try {
|
try {
|
||||||
el.currentTime = relTime;
|
el.currentTime = relTime;
|
||||||
} catch {
|
} catch (err) {
|
||||||
// ignore — the seek will be retried on the next tick
|
// ignore — the seek will be retried on the next tick
|
||||||
|
swallow("runtime.media.site3", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// After a hard seek, clear the in-flight play guard so the next tick
|
// After a hard seek, clear the in-flight play guard so the next tick
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { RuntimeJson, RuntimeOutboundMessage, RuntimePickerElementInfo } from "./types";
|
import type { RuntimeJson, RuntimeOutboundMessage, RuntimePickerElementInfo } from "./types";
|
||||||
|
import { swallow } from "./diagnostics";
|
||||||
|
|
||||||
type PickerModuleDeps = {
|
type PickerModuleDeps = {
|
||||||
postMessage: (payload: RuntimeOutboundMessage) => void;
|
postMessage: (payload: RuntimeOutboundMessage) => void;
|
||||||
@@ -33,8 +34,9 @@ export function createPickerModule(deps: PickerModuleDeps): PickerModule {
|
|||||||
function emitPickerRuntimeEvent(eventName: string, detail: RuntimeJson): void {
|
function emitPickerRuntimeEvent(eventName: string, detail: RuntimeJson): void {
|
||||||
try {
|
try {
|
||||||
window.dispatchEvent(new CustomEvent(eventName, { detail }));
|
window.dispatchEvent(new CustomEvent(eventName, { detail }));
|
||||||
} catch {
|
} catch (err) {
|
||||||
// no-op in unsupported contexts
|
// no-op in unsupported contexts
|
||||||
|
swallow("runtime.picker.site1", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { RuntimePlayer, RuntimeTimelineLike } from "./types";
|
import type { RuntimePlayer, RuntimeTimelineLike } from "./types";
|
||||||
import { quantizeTimeToFrame } from "../inline-scripts/parityContract";
|
import { quantizeTimeToFrame } from "../inline-scripts/parityContract";
|
||||||
|
import { swallow } from "./diagnostics";
|
||||||
|
|
||||||
type PlayerDeps = {
|
type PlayerDeps = {
|
||||||
getTimeline: () => RuntimeTimelineLike | null;
|
getTimeline: () => RuntimeTimelineLike | null;
|
||||||
@@ -38,8 +39,9 @@ function forEachSiblingTimeline(
|
|||||||
if (!tl || tl === master) continue;
|
if (!tl || tl === master) continue;
|
||||||
try {
|
try {
|
||||||
fn(tl);
|
fn(tl);
|
||||||
} catch {
|
} catch (err) {
|
||||||
// ignore sibling failures — one broken timeline shouldn't poison play/pause
|
// ignore sibling failures — one broken timeline shouldn't poison play/pause
|
||||||
|
swallow("runtime.player.site1", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -76,8 +78,9 @@ function seekMasterAndSiblingTimelinesDeterministically(
|
|||||||
for (const tl of rearmedSiblings) {
|
for (const tl of rearmedSiblings) {
|
||||||
try {
|
try {
|
||||||
tl.pause();
|
tl.pause();
|
||||||
} catch {
|
} catch (err) {
|
||||||
// ignore sibling failures — one broken timeline shouldn't poison seek
|
// ignore sibling failures — one broken timeline shouldn't poison seek
|
||||||
|
swallow("runtime.player.site2", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { RuntimeTimelineLike } from "./types";
|
import type { RuntimeTimelineLike } from "./types";
|
||||||
|
import { swallow } from "./diagnostics";
|
||||||
|
|
||||||
const AUTHORED_DURATION_ATTR = "data-hf-authored-duration";
|
const AUTHORED_DURATION_ATTR = "data-hf-authored-duration";
|
||||||
const AUTHORED_END_ATTR = "data-hf-authored-end";
|
const AUTHORED_END_ATTR = "data-hf-authored-end";
|
||||||
@@ -118,8 +119,9 @@ export function createRuntimeStartTimeResolver(params: {
|
|||||||
if (Number.isFinite(timelineDuration) && timelineDuration > 0) {
|
if (Number.isFinite(timelineDuration) && timelineDuration > 0) {
|
||||||
resolved = timelineDuration;
|
resolved = timelineDuration;
|
||||||
}
|
}
|
||||||
} catch {
|
} catch (err) {
|
||||||
// ignore broken timeline impls
|
// ignore broken timeline impls
|
||||||
|
swallow("runtime.startResolver.site1", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import type {
|
|||||||
RuntimeTimelineScene,
|
RuntimeTimelineScene,
|
||||||
RuntimeTimelineLike,
|
RuntimeTimelineLike,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
import { swallow } from "./diagnostics";
|
||||||
import { createRuntimeStartTimeResolver } from "./startResolver";
|
import { createRuntimeStartTimeResolver } from "./startResolver";
|
||||||
|
|
||||||
const AUTHORED_DURATION_ATTR = "data-hf-authored-duration";
|
const AUTHORED_DURATION_ATTR = "data-hf-authored-duration";
|
||||||
@@ -565,8 +566,9 @@ export function collectRuntimeTimelinePayload(params: {
|
|||||||
});
|
});
|
||||||
gsapClipIds.add(el.id);
|
gsapClipIds.add(el.id);
|
||||||
}
|
}
|
||||||
} catch {
|
} catch (err) {
|
||||||
// GSAP introspection is best-effort — don't break timeline if it fails
|
// GSAP introspection is best-effort — don't break timeline if it fails
|
||||||
|
swallow("runtime.timeline.site1", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user