diff --git a/packages/studio/src/player/lib/timelineIframeHelpers.test.ts b/packages/studio/src/player/lib/timelineIframeHelpers.test.ts
index 7383a4ae31..b49a6e9593 100644
--- a/packages/studio/src/player/lib/timelineIframeHelpers.test.ts
+++ b/packages/studio/src/player/lib/timelineIframeHelpers.test.ts
@@ -118,6 +118,47 @@ describe("buildMissingCompositionElements — hfId (R7)", () => {
});
});
+describe("buildMissingCompositionElements — on a settled timeline", () => {
+ // Studio runs this on every runtime state tick, so a timeline with nothing to add must cost one host scan.
+ const settled = () =>
+ makeDoc(`
+
+
+ ${Array.from({ length: 40 }, (_, i) => `
`).join("")}
+
+ `);
+ const rows = ["intro", ...Array.from({ length: 40 }, (_, i) => `clip-${i}`)].map((id, i) => ({
+ id,
+ key: id,
+ tag: id === "intro" ? "div" : "video",
+ start: i,
+ duration: 1,
+ track: 0,
+ }));
+
+ it("walks no more of the document than the hosts when nothing is missing or patchable", () => {
+ const doc = settled();
+ const scans = vi.spyOn(doc, "querySelectorAll");
+ const result = buildMissingCompositionElements(doc, window as IframeWindow, rows, 40);
+ expect(scans.mock.calls.map(([selector]) => selector)).toEqual([
+ "[data-composition-id][data-start]",
+ ]);
+ expect([result.missing, result.patched, result.updatedEls]).toEqual([[], false, rows]);
+ });
+
+ it("still patches a row whose host carries its source", () => {
+ const doc = settled();
+ doc.getElementById("intro")!.setAttribute("data-composition-src", "compositions/intro.html");
+ const { patched, updatedEls } = buildMissingCompositionElements(
+ doc,
+ window as IframeWindow,
+ rows,
+ 40,
+ );
+ expect([patched, updatedEls[0]?.compositionSrc]).toEqual([true, "compositions/intro.html"]);
+ });
+});
+
describe("buildMissingCompositionElements — nested master time", () => {
it("places hosts two and three levels deep at their master time", () => {
const doc = makeDoc(`
diff --git a/packages/studio/src/player/lib/timelineIframeHelpers.ts b/packages/studio/src/player/lib/timelineIframeHelpers.ts
index dd75c6d20e..0ee8ce1d60 100644
--- a/packages/studio/src/player/lib/timelineIframeHelpers.ts
+++ b/packages/studio/src/player/lib/timelineIframeHelpers.ts
@@ -493,7 +493,7 @@ export function buildMissingCompositionElements(
const hosts = doc.querySelectorAll("[data-composition-id][data-start]");
const missing: TimelineElement[] = [];
- const resolveEnd = createReferenceEndResolver(createTimedElementLookup(doc), iframeWin);
+ let resolveEnd: ReturnType | undefined;
const masterTime = createRuntimeStartTimeResolver({
timelineRegistry: iframeWin.__timelines,
includeAuthoredTimingAttrs: true,
@@ -505,6 +505,7 @@ export function buildMissingCompositionElements(
const compId = el.getAttribute("data-composition-id");
if (!compId || compId === rootCompId) continue;
if (existingIds.has(el.id) || existingIds.has(compId)) continue;
+ resolveEnd ??= createReferenceEndResolver(createTimedElementLookup(doc), iframeWin);
const entry = buildMissingCompositionEntry({
doc,
iframeWin,
@@ -518,7 +519,9 @@ export function buildMissingCompositionElements(
if (entry) missing.push(entry);
}
- // Patch existing elements that are missing compositionSrc
+ // Patch existing elements that are missing compositionSrc; only a host carrying its source can supply one.
+ if (!doc.querySelector("[data-composition-src], [data-composition-file]"))
+ return { missing, updatedEls: currentEls as TimelineElement[], patched: false };
let patched = false;
const findHost = previewElementFinder(doc);
const updatedEls = (currentEls as TimelineElement[]).map((existing) => {