refactor(core): simplify packages/core — dead code, dedup, type safety (#1413)

- Delete unused mediaPreloader module, 5 dead RuntimeState fields,
  emitPerformanceMetric, lintScriptUrls, 5 variable type guards
- Consolidate compiler utilities: unify CSS URL regex, relative URL
  predicate, MIME map, @import regex, bulk asset rewrite delegation
- Cache extractGsapWindows per script (eliminates 2 redundant recast
  parses per lint run), share stripJsComments and script extraction
- Deduplicate GSAP parser: share serializeValue/safeJsKey, centralize
  converted-id fallback (6 sites), keyframe codegen (3 sites),
  waypoint extraction, insert-after-anchor, script hoisting
- Replace 88 bare any annotations with typed AstNode/AstPath interfaces
- Derive RuntimeBridgeControlAction from HyperframeControlAction,
  alias RuntimePickerElementInfo, share macOS font profiler
- Gate generateHyperframesStyles on includeStyles, collapse 4 GSAP
  property mutation cases into 2
- Extract magic numbers into named constants, replace 5 double casts
  with type guards and typed accessors (runtime/globals.ts),
  reduce function complexity in htmlParser and files route
This commit is contained in:
Miguel Ángel
2026-06-13 18:23:36 -04:00
committed by GitHub
parent fbc3cdf2fd
commit 6f677292ae
29 changed files with 551 additions and 1399 deletions
+1 -92
View File
@@ -1,5 +1,5 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { initRuntimeAnalytics, emitAnalyticsEvent, emitPerformanceMetric } from "./analytics";
import { initRuntimeAnalytics, emitAnalyticsEvent } from "./analytics";
describe("runtime analytics", () => {
let postMessage: ReturnType<typeof vi.fn>;
@@ -58,94 +58,3 @@ describe("runtime analytics", () => {
expect(postMessage).toHaveBeenCalledTimes(events.length);
});
});
describe("runtime performance metrics", () => {
let postMessage: ReturnType<typeof vi.fn>;
beforeEach(() => {
postMessage = vi.fn();
initRuntimeAnalytics(postMessage);
// Clean up DevTools marks between tests to avoid cross-test interference.
if (typeof performance !== "undefined" && typeof performance.clearMarks === "function") {
performance.clearMarks();
}
});
it("emits a perf metric via postMessage", () => {
emitPerformanceMetric("player_scrub_latency", 12.5);
expect(postMessage).toHaveBeenCalledWith({
source: "hf-preview",
type: "perf",
name: "player_scrub_latency",
value: 12.5,
tags: {},
});
});
it("passes tags through", () => {
emitPerformanceMetric("player_decoder_count", 3, {
composition_id: "abc123",
mode: "isolated",
});
expect(postMessage).toHaveBeenCalledWith({
source: "hf-preview",
type: "perf",
name: "player_decoder_count",
value: 3,
tags: { composition_id: "abc123", mode: "isolated" },
});
});
it("normalizes missing tags to an empty object", () => {
emitPerformanceMetric("player_playback_fps", 60);
expect(postMessage).toHaveBeenCalledWith(expect.objectContaining({ tags: {} }));
});
it("supports zero and negative values", () => {
emitPerformanceMetric("player_dropped_frames", 0);
emitPerformanceMetric("player_media_sync_drift", -8.3);
expect(postMessage).toHaveBeenNthCalledWith(1, expect.objectContaining({ value: 0 }));
expect(postMessage).toHaveBeenNthCalledWith(2, expect.objectContaining({ value: -8.3 }));
});
it("does not throw when postMessage is not set", () => {
initRuntimeAnalytics(null as unknown as (payload: unknown) => void);
expect(() => emitPerformanceMetric("player_load_time", 250)).not.toThrow();
});
it("does not throw when postMessage throws", () => {
postMessage.mockImplementation(() => {
throw new Error("channel closed");
});
expect(() => emitPerformanceMetric("player_scrub_latency", 12)).not.toThrow();
});
it("does not throw when performance.mark throws", () => {
const original = performance.mark;
// Vitest provides a real performance API; replace mark with a thrower for this test.
performance.mark = vi.fn(() => {
throw new Error("mark failed");
}) as typeof performance.mark;
try {
expect(() => emitPerformanceMetric("player_load_time", 100)).not.toThrow();
// Even though performance.mark threw, the bridge should still receive the metric.
expect(postMessage).toHaveBeenCalledWith(
expect.objectContaining({ type: "perf", name: "player_load_time", value: 100 }),
);
} finally {
performance.mark = original;
}
});
it("writes a User Timing mark with detail for DevTools visibility", () => {
if (typeof performance.getEntriesByName !== "function") {
// Older test environments — skip the DevTools assertion but don't fail.
return;
}
emitPerformanceMetric("player_composition_switch", 42, { from: "a", to: "b" });
const entries = performance.getEntriesByName("player_composition_switch", "mark");
expect(entries.length).toBeGreaterThan(0);
const mark = entries[entries.length - 1] as PerformanceMark;
expect(mark.detail).toEqual({ value: 42, tags: { from: "a", to: "b" } });
});
});
+3 -83
View File
@@ -1,33 +1,9 @@
import { swallow } from "./diagnostics";
/**
* Runtime analytics & performance telemetry — vendor-agnostic event emission.
* Runtime analytics — vendor-agnostic event emission via postMessage.
*
* The runtime emits structured events via postMessage. The host application
* decides what to do with them: forward to PostHog, Mixpanel, Amplitude,
* a custom logger, or nothing at all.
*
* For session replay: initialize your analytics SDK (e.g. PostHog) only in
* the parent app with `recordCrossOriginIframes: true`. No SDK needs to run
* inside this iframe.
*
* ## Host app integration
*
* ```javascript
* window.addEventListener("message", (e) => {
* if (e.data?.source !== "hf-preview") return;
*
* if (e.data.type === "analytics") {
* // discrete lifecycle events: composition_loaded, played, seeked, etc.
* posthog.capture(e.data.event, e.data.properties);
* }
*
* if (e.data.type === "perf") {
* // numeric performance metrics: scrub latency, fps, decoder count, etc.
* // Aggregate per-session (p50/p95) and forward on flush.
* myMetrics.observe(e.data.name, e.data.value, e.data.tags);
* }
* });
* ```
* The host application decides what to do with events: forward to PostHog,
* Mixpanel, Amplitude, a custom logger, or nothing at all.
*/
export type RuntimeAnalyticsEvent =
@@ -40,16 +16,7 @@ export type RuntimeAnalyticsEvent =
export type RuntimeAnalyticsProperties = Record<string, string | number | boolean | null>;
/**
* Tags attached to a performance metric — small, low-cardinality identifiers
* (composition id hash, media count bucket, browser version, etc.). Same shape
* as analytics properties so hosts can forward both through one pipeline.
*/
export type RuntimePerformanceTags = Record<string, string | number | boolean | null>;
// Stored reference to the postRuntimeMessage function, set during init.
// Avoids a circular import between analytics ↔ bridge. Shared by both
// emitAnalyticsEvent and emitPerformanceMetric — one bridge, two channels.
let _postMessage: ((payload: unknown) => void) | null = null;
/**
@@ -81,50 +48,3 @@ export function emitAnalyticsEvent(
swallow("runtime.analytics.site1", err);
}
}
/**
* Emit a numeric performance metric through the bridge.
*
* Used for player-perf telemetry — scrub latency, sustained fps, dropped
* frames, decoder count, composition load time, media sync drift. The host
* aggregates per-session values (p50/p95) and forwards to its observability
* pipeline on flush.
*
* Also writes a `performance.mark()` so the metric shows up under the
* DevTools Performance panel's "User Timing" track for local debugging,
* with `value` and `tags` available on the entry's `detail` field.
*
* @param name Metric name, e.g. "player_scrub_latency", "player_playback_fps"
* @param value Numeric value (units are metric-specific: ms for latency, fps for rate, etc.)
* @param tags Optional low-cardinality tags (composition id, media count bucket, etc.)
*/
export function emitPerformanceMetric(
name: string,
value: number,
tags?: RuntimePerformanceTags,
): void {
// Local DevTools breadcrumb. Wrapped because performance.mark() can throw on
// strict CSP, when the document is not yet ready, or when `detail` is non-cloneable.
try {
if (typeof performance !== "undefined" && typeof performance.mark === "function") {
performance.mark(name, { detail: { value, tags: tags ?? {} } });
}
} catch (err) {
// performance API unavailable or rejected — keep going
swallow("runtime.analytics.site2", err);
}
if (!_postMessage) return;
try {
_postMessage({
source: "hf-preview",
type: "perf",
name,
value,
tags: tags ?? {},
});
} catch (err) {
// Never let telemetry failures affect the runtime
swallow("runtime.analytics.site3", err);
}
}
+2 -15
View File
@@ -27,24 +27,11 @@
* 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;
};
}
import { getDebugSurface } from "./globals.js";
export function swallow(label: string, error?: unknown): void {
if (typeof window === "undefined") return;
const w = window as unknown as HFDebugSurface;
const w = getDebugSurface();
const handler = w.__hf?.onSwallowed;
if (handler) {
+11
View File
@@ -0,0 +1,11 @@
export interface HFDebugSurface {
__hfDebug?: boolean;
__HYPERFRAMES_DEBUG?: boolean;
__hf?: {
onSwallowed?: (event: { label: string; error: unknown }) => void;
};
}
export function getDebugSurface(): HFDebugSurface {
return globalThis as HFDebugSurface;
}
@@ -1,248 +0,0 @@
import { describe, it, expect, beforeEach, vi } from "vitest";
import { createMediaPreloadManager } from "./mediaPreloader";
function mockMediaElement(attrs: {
start: string;
duration?: string;
tag?: string;
}): HTMLMediaElement {
const el = {
tagName: (attrs.tag ?? "VIDEO").toUpperCase(),
preload: "auto",
readyState: 0,
duration: Number.NaN,
defaultPlaybackRate: 1,
loop: false,
src: `blob:mock-${attrs.start}`,
dataset: {
start: attrs.start,
duration: attrs.duration,
},
hasAttribute: (name: string) => name === "data-start",
getAttribute: (name: string) => {
if (name === "data-start") return attrs.start;
if (name === "data-duration") return attrs.duration ?? null;
return null;
},
removeAttribute: (name: string) => {
if (name === "src") {
(el as Record<string, unknown>).src = "";
}
},
closest: () => null,
load: vi.fn(),
} as unknown as HTMLMediaElement;
return el;
}
function setupDOM(elements: HTMLMediaElement[]): void {
const originalQuerySelector = document.querySelectorAll.bind(document);
document.querySelectorAll = ((selector: string) => {
if (selector === "video, audio") return elements as unknown as NodeListOf<Element>;
return originalQuerySelector(selector);
}) as typeof document.querySelectorAll;
}
function createTestFixture(
count: number,
options?: Parameters<typeof createMediaPreloadManager>[0],
) {
const elements = Array.from({ length: count }, (_, i) =>
mockMediaElement({ start: String(i * 5), duration: "5" }),
);
setupDOM(elements);
const manager = createMediaPreloadManager(options);
manager.refresh();
return { elements, manager };
}
describe("createMediaPreloadManager", () => {
let elements: HTMLMediaElement[];
beforeEach(() => {
elements = [];
});
it("is not lazy when fewer than 3 media elements", () => {
elements = [
mockMediaElement({ start: "0", duration: "5" }),
mockMediaElement({ start: "5", duration: "5" }),
];
setupDOM(elements);
const manager = createMediaPreloadManager();
manager.refresh();
expect(manager.isLazy()).toBe(false);
});
it("activates lazy mode at exactly LAZY_THRESHOLD (3 elements)", () => {
const { manager } = createTestFixture(3);
expect(manager.isLazy()).toBe(true);
});
it("is not lazy with 2 elements (below threshold)", () => {
const { manager } = createTestFixture(2);
expect(manager.isLazy()).toBe(false);
});
it("activates lazy mode with 8 media elements", () => {
const { manager } = createTestFixture(8);
expect(manager.isLazy()).toBe(true);
});
it("activates lazy mode for 4-5 clip compositions without spurious eviction", () => {
const f = createTestFixture(4);
expect(f.manager.isLazy()).toBe(true);
for (const el of f.elements) el.preload = "metadata";
f.manager.sync(0);
const promoted = f.elements.filter((el) => el.preload === "auto").length;
expect(promoted).toBeGreaterThanOrEqual(2);
expect(promoted).toBeLessThanOrEqual(4);
f.manager.sync(0);
const evicted = f.elements.filter(
(el) =>
el.preload === "metadata" && (el.load as ReturnType<typeof vi.fn>).mock.calls.length > 1,
);
expect(evicted.length).toBe(0);
});
it("sync promotes clips in the lookahead window", () => {
const f = createTestFixture(8);
for (const el of f.elements) el.preload = "metadata";
f.manager.sync(0);
expect(f.elements[0].preload).toBe("auto");
expect(f.elements[1].preload).toBe("auto");
expect(f.elements[7].preload).toBe("metadata");
});
it("preloadAroundTime promotes clips near seek target", () => {
const f = createTestFixture(10);
for (const el of f.elements) el.preload = "metadata";
f.manager.preloadAroundTime(30);
expect(f.elements[6].preload).toBe("auto");
expect(f.elements[7].preload).toBe("auto");
expect(f.elements[0].preload).toBe("metadata");
});
it("sync is a no-op when not lazy", () => {
const f = createTestFixture(2);
f.manager.sync(0);
expect(f.manager.isLazy()).toBe(false);
});
it("guarantees at least LOOKAHEAD_MIN_CLIPS are promoted", () => {
// Use 20s spacing so only 1 clip falls in the 10s lookahead window
elements = Array.from({ length: 8 }, (_, i) =>
mockMediaElement({ start: String(i * 20), duration: "5" }),
);
setupDOM(elements);
const manager = createMediaPreloadManager();
manager.refresh();
for (const el of elements) el.preload = "metadata";
manager.sync(0);
expect(elements.filter((el) => el.preload === "auto").length).toBeGreaterThanOrEqual(2);
});
it("evicts clips when scrubbing away from them", () => {
const f = createTestFixture(10);
for (const el of f.elements) el.preload = "metadata";
f.manager.sync(0);
expect(f.elements[0].preload).toBe("auto");
f.manager.sync(40);
expect(f.elements[0].preload).toBe("metadata");
expect(f.elements[0].src).toBe("");
expect(f.elements[8].preload).toBe("auto");
});
it("restores src when re-promoting a previously evicted clip", () => {
const f = createTestFixture(10);
for (const el of f.elements) el.preload = "metadata";
const originalSrc0 = f.elements[0].src;
f.manager.sync(0);
f.manager.sync(40);
expect(f.elements[0].src).toBe("");
f.manager.sync(0);
expect(f.elements[0].src).toBe(originalSrc0);
expect(f.elements[0].preload).toBe("auto");
});
it("does not exceed MAX_PROMOTED (5) clips", () => {
const f = createTestFixture(10);
for (const el of f.elements) el.preload = "metadata";
f.manager.sync(0);
expect(f.elements.filter((el) => el.preload === "auto").length).toBeLessThanOrEqual(5);
f.manager.sync(25);
expect(f.elements.filter((el) => el.preload === "auto").length).toBeLessThanOrEqual(5);
});
it("calls load() when evicting to release buffers", () => {
const f = createTestFixture(10);
for (const el of f.elements) el.preload = "metadata";
f.manager.sync(0);
const loadCallsBefore = (f.elements[0].load as ReturnType<typeof vi.fn>).mock.calls.length;
f.manager.sync(40);
expect((f.elements[0].load as ReturnType<typeof vi.fn>).mock.calls.length).toBeGreaterThan(
loadCallsBefore,
);
});
it("isLazy reports true with 6+ clips so caller can gate render-mode bypass", () => {
const { manager } = createTestFixture(6);
expect(manager.isLazy()).toBe(true);
});
it("calls onActivation when lazy mode activates", () => {
const onActivation = vi.fn();
createTestFixture(8, { onActivation });
expect(onActivation).toHaveBeenCalledOnce();
expect(onActivation).toHaveBeenCalledWith(8);
});
it("does not call onActivation below threshold", () => {
const onActivation = vi.fn();
createTestFixture(2, { onActivation });
expect(onActivation).not.toHaveBeenCalled();
});
it("calls onActivation only once across multiple refreshes", () => {
const onActivation = vi.fn();
const { manager } = createTestFixture(8, { onActivation });
manager.refresh();
manager.refresh();
expect(onActivation).toHaveBeenCalledOnce();
});
it("respects window.__HF_LAZY_PRELOAD_THRESHOLD override", () => {
elements = Array.from({ length: 2 }, (_, i) =>
mockMediaElement({ start: String(i * 5), duration: "5" }),
);
setupDOM(elements);
// 2 elements is below the default threshold (3) but at our custom one
(window as Record<string, unknown>).__HF_LAZY_PRELOAD_THRESHOLD = 2;
const manager = createMediaPreloadManager();
manager.refresh();
expect(manager.isLazy()).toBe(true);
// Clean up
delete (window as Record<string, unknown>).__HF_LAZY_PRELOAD_THRESHOLD;
});
it("falls back to default threshold when __HF_LAZY_PRELOAD_THRESHOLD is not set", () => {
elements = Array.from({ length: 2 }, (_, i) =>
mockMediaElement({ start: String(i * 5), duration: "5" }),
);
setupDOM(elements);
// Ensure it's not set
delete (window as Record<string, unknown>).__HF_LAZY_PRELOAD_THRESHOLD;
const manager = createMediaPreloadManager();
manager.refresh();
expect(manager.isLazy()).toBe(false);
});
});
-170
View File
@@ -1,170 +0,0 @@
import { refreshRuntimeMediaCache, type RuntimeMediaClip } from "./media";
// Start lazy preload management at 3 clips to keep memory pressure low from
// the start. The previous threshold of 6 let medium compositions (45 heavy
// videos) saturate browser memory before the preloader kicked in.
const LAZY_THRESHOLD = 3;
const LOOKAHEAD_SECONDS = 10;
const LOOKBEHIND_SECONDS = 3;
const LOOKAHEAD_MIN_CLIPS = 2;
// Adaptive cap: base of 4 for small sets, clamped to 6 for larger ones.
// The window-based eviction in syncWindow() is the primary memory bound;
// this cap is defense-in-depth for compositions with many short clips
// packed into the lookahead window.
const MAX_PROMOTED_BASE = 4;
const MAX_PROMOTED_CEIL = 6;
export interface MediaPreloadManager {
refresh(): void;
sync(currentTimeSeconds: number): void;
preloadAroundTime(timeSeconds: number): void;
isLazy(): boolean;
}
export function createMediaPreloadManager(options?: {
resolveStartSeconds?: (element: Element) => number;
resolveDurationSeconds?: (element: HTMLVideoElement | HTMLAudioElement) => number | null;
shouldIncludeElement?: (element: HTMLVideoElement | HTMLAudioElement) => boolean;
onActivation?: (clipCount: number) => void;
}): MediaPreloadManager {
let clips: RuntimeMediaClip[] = [];
const promoted = new Set<HTMLMediaElement>();
/** Insertion-order queue for LRU eviction (oldest first). */
const promotionOrder: HTMLMediaElement[] = [];
/** Stashed original src so we can restore after eviction. */
const originalSrc = new Map<HTMLMediaElement, string>();
let lazy = false;
let activationEmitted = false;
function refresh(): void {
const cache = refreshRuntimeMediaCache(options);
clips = cache.mediaClips;
const configuredThreshold =
typeof (window as Record<string, unknown>).__HF_LAZY_PRELOAD_THRESHOLD === "number"
? ((window as Record<string, unknown>).__HF_LAZY_PRELOAD_THRESHOLD as number)
: LAZY_THRESHOLD;
lazy = clips.length >= configuredThreshold;
if (lazy && !activationEmitted) {
activationEmitted = true;
options?.onActivation?.(clips.length);
}
}
function evictClip(clip: RuntimeMediaClip): void {
if (!promoted.has(clip.el)) return;
// Stash original src before clearing
if (!originalSrc.has(clip.el)) {
originalSrc.set(clip.el, clip.el.src);
}
// Release buffered data: only way to free memory per MDN
clip.el.removeAttribute("src");
clip.el.load();
clip.el.preload = "metadata";
promoted.delete(clip.el);
const idx = promotionOrder.indexOf(clip.el);
if (idx !== -1) promotionOrder.splice(idx, 1);
}
function promoteClip(clip: RuntimeMediaClip): void {
if (promoted.has(clip.el)) return;
// Restore src if previously evicted
const stashedSrc = originalSrc.get(clip.el);
if (stashedSrc !== undefined && !clip.el.src) {
clip.el.src = stashedSrc;
originalSrc.delete(clip.el);
}
promoted.add(clip.el);
promotionOrder.push(clip.el);
if (clip.el.preload !== "auto") {
clip.el.preload = "auto";
}
if (clip.el.readyState < HTMLMediaElement.HAVE_FUTURE_DATA) {
clip.el.load();
}
}
function evictOutsideWindow(inWindow: Set<RuntimeMediaClip>): void {
const windowEls = new Set<HTMLMediaElement>();
for (const clip of inWindow) {
windowEls.add(clip.el);
}
for (const clip of clips) {
if (promoted.has(clip.el) && !windowEls.has(clip.el)) {
evictClip(clip);
}
}
const maxPromoted = Math.min(
MAX_PROMOTED_CEIL,
MAX_PROMOTED_BASE + Math.floor(clips.length / 10),
);
while (promotionOrder.length > maxPromoted) {
const oldest = promotionOrder[0];
if (windowEls.has(oldest)) break;
const clip = clips.find((c) => c.el === oldest);
if (clip) {
evictClip(clip);
} else {
promoted.delete(oldest);
promotionOrder.shift();
}
}
}
function getClipsInWindow(timeSeconds: number): Set<RuntimeMediaClip> {
const windowStart = timeSeconds - LOOKBEHIND_SECONDS;
const windowEnd = timeSeconds + LOOKAHEAD_SECONDS;
const inWindow = new Set<RuntimeMediaClip>();
for (const clip of clips) {
const active = timeSeconds >= clip.start && timeSeconds < clip.end;
const inLookahead = clip.start >= timeSeconds && clip.start <= windowEnd;
const inLookbehind = clip.end > windowStart && clip.end <= timeSeconds;
if (active || inLookahead || inLookbehind) {
inWindow.add(clip);
}
}
if (inWindow.size < LOOKAHEAD_MIN_CLIPS) {
const sorted = clips
.filter((c) => c.start >= timeSeconds && !inWindow.has(c))
.sort((a, b) => a.start - b.start);
for (const clip of sorted) {
inWindow.add(clip);
if (inWindow.size >= LOOKAHEAD_MIN_CLIPS) break;
}
}
return inWindow;
}
function syncWindow(timeSeconds: number): void {
const window = getClipsInWindow(timeSeconds);
evictOutsideWindow(window);
for (const clip of clips) {
if (window.has(clip)) {
promoteClip(clip);
}
}
}
function sync(currentTimeSeconds: number): void {
if (!lazy) return;
syncWindow(currentTimeSeconds);
}
function preloadAroundTime(timeSeconds: number): void {
if (!lazy) return;
syncWindow(timeSeconds);
}
function isLazy(): boolean {
return lazy;
}
return { refresh, sync, preloadAroundTime, isLazy };
}
-2
View File
@@ -10,9 +10,7 @@ describe("createRuntimeState", () => {
expect(state.playbackRate).toBe(1);
expect(state.bridgeMuted).toBe(false);
expect(state.capturedTimeline).toBeNull();
expect(state.rafId).toBeNull();
expect(state.tornDown).toBe(false);
expect(state.parityModeEnabled).toBe(true);
});
it("returns independent instances", () => {
-10
View File
@@ -5,10 +5,8 @@ import type { TransportClock } from "./clock";
export type RuntimeState = {
capturedTimeline: RuntimeTimelineLike | null;
isPlaying: boolean;
rafId: number | null;
currentTime: number;
deterministicAdapters: RuntimeDeterministicAdapter[];
parityModeEnabled: boolean;
canonicalFps: number;
bridgeMuted: boolean;
bridgeVolume: number;
@@ -62,9 +60,7 @@ export type RuntimeState = {
*/
bridgeMaxPostIntervalMs: number;
controlBridgeHandler: ((event: MessageEvent) => void) | null;
clampDurationLoggedRaw: number | null;
beforeUnloadHandler: (() => void) | null;
domReadyHandler: (() => void) | null;
injectedCompStyles: HTMLStyleElement[];
injectedCompScripts: HTMLScriptElement[];
cachedTimedMediaEls: Array<HTMLVideoElement | HTMLAudioElement>;
@@ -72,7 +68,6 @@ export type RuntimeState = {
cachedVideoClips: RuntimeMediaClip[];
cachedMediaTimelineDurationSeconds: number;
tornDown: boolean;
nativeVisualWatchdogTick: number;
/**
* Single-clock transport. The sole time authority — GSAP is always
* paused and seeked to `clock.now()` on each rAF tick. Eliminates
@@ -87,10 +82,8 @@ export function createRuntimeState(): RuntimeState {
return {
capturedTimeline: null,
isPlaying: false,
rafId: null,
currentTime: 0,
deterministicAdapters: [],
parityModeEnabled: true,
canonicalFps: 30,
bridgeMuted: false,
bridgeVolume: 1,
@@ -104,9 +97,7 @@ export function createRuntimeState(): RuntimeState {
bridgeLastPostedMuted: false,
bridgeMaxPostIntervalMs: 80,
controlBridgeHandler: null,
clampDurationLoggedRaw: null,
beforeUnloadHandler: null,
domReadyHandler: null,
injectedCompStyles: [],
injectedCompScripts: [],
cachedTimedMediaEls: [],
@@ -114,7 +105,6 @@ export function createRuntimeState(): RuntimeState {
cachedVideoClips: [],
cachedMediaTimelineDurationSeconds: 0,
tornDown: false,
nativeVisualWatchdogTick: 0,
transportClock: null,
transportRafId: null,
};
+5 -24
View File
@@ -6,17 +6,14 @@ export type RuntimeJson =
| RuntimeJson[]
| { [key: string]: RuntimeJson };
import type { HyperframeControlAction } from "../inline-scripts/runtimeContract.js";
import type { HyperframePickerElementInfo } from "../inline-scripts/pickerApi.js";
export type RuntimeBridgeControlAction =
| "play"
| "pause"
| "seek"
| HyperframeControlAction
| "tick"
| "set-muted"
| "set-volume"
| "set-media-output-muted"
| "set-playback-rate"
| "enable-pick-mode"
| "disable-pick-mode"
| "flash-elements";
export type RuntimeBridgeControlMessage = {
@@ -85,23 +82,7 @@ export type RuntimeDiagnosticMessage = {
details: Record<string, RuntimeJson>;
};
export type RuntimePickerBoundingBox = {
x: number;
y: number;
width: number;
height: number;
};
export type RuntimePickerElementInfo = {
id: string | null;
tagName: string;
selector: string;
label: string;
boundingBox: RuntimePickerBoundingBox;
textContent: string | null;
src: string | null;
dataAttributes: Record<string, string>;
};
export type RuntimePickerElementInfo = HyperframePickerElementInfo;
export type RuntimePickerHoveredMessage = {
source: "hf-preview";