@hyperframes/engine 0.8.142 → 0.8.144

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.
@@ -971,6 +971,7 @@ async function constructCaptureSession(input) {
971
971
  browserConsoleBuffer: [],
972
972
  scriptLoadFailures: [],
973
973
  pageErrors: [],
974
+ subTimelineWaitMemo: sessionOptions.subTimelineWaitMemo ?? {},
974
975
  warnings: [],
975
976
  capturePerf: {
976
977
  frames: 0,
@@ -1166,19 +1167,44 @@ async function pollHfReady(page, timeoutMs, intervalMs = 100) {
1166
1167
  ` State: __hf=${diag.hasHf}, seek=${diag.hasSeek}, player=${diag.hasPlayer}, ` +
1167
1168
  `renderReady=${diag.renderReady}, duration=${diag.duration}`);
1168
1169
  }
1169
- export async function pollSubCompositionTimelines(page, timeoutMs, intervalMs = 150,
1170
- // Fail-fast hook: when a SCRIPT resource failed to load (404 / request
1171
- // failure), the timeline registration it carried can never arrive — the
1172
- // full-timeout wait buys nothing (measured: a 705-render spike at the 45s
1173
- // setup bucket in 30 days of wild local renders, ~1% of renders, each also
1174
- // shipping silently-broken animations). Once failures are present the poll
1175
- // is cut to `scriptFailureGraceMs` from its start.
1176
- getScriptLoadFailures, scriptFailureGraceMs = 2_000,
1177
- // Reports the composition ids still unregistered at bail time, so the caller
1178
- // can put them in the structured warning as well as in stderr.
1179
- onPending,
1180
- // Ends the wait at once; the caller then fails the render for its own reason.
1181
- shouldStop) {
1170
+ // Enumerated regardless of bail reason, so the warning names every composition still
1171
+ // waited on, not only a failed script URL.
1172
+ async function listUnregisteredTimelineIds(page) {
1173
+ const evaluated = await page.evaluate(`(function() {
1174
+ var hosts = document.querySelectorAll("[data-composition-id]");
1175
+ var timelines = window.__timelines || {};
1176
+ var m = [];
1177
+ for (var i = 0; i < hosts.length; i++) {
1178
+ if (hosts[i].hasAttribute("data-no-timeline")) continue;
1179
+ var id = hosts[i].getAttribute("data-composition-id");
1180
+ if (id && !timelines[id]) m.push(id);
1181
+ }
1182
+ return m;
1183
+ })()`);
1184
+ // Builds a warning, so it must never throw: normalise a loosely typed result.
1185
+ return Array.isArray(evaluated) ? evaluated.map((id) => String(id)) : [];
1186
+ }
1187
+ export async function waitForSubCompositionTimelines(session, page, timeoutMs) {
1188
+ const memo = session.subTimelineWaitMemo;
1189
+ session.subTimelineWaitOutcome = await pollSubCompositionTimelines(page, timeoutMs, {
1190
+ getScriptLoadFailures: () => session.scriptLoadFailures,
1191
+ onPending: (ids) => {
1192
+ session.pendingTimelineIds = [...ids];
1193
+ },
1194
+ shouldStop: () => session.vfxFailure !== undefined,
1195
+ knownUnregisteredIds: memo.unregisteredIds,
1196
+ });
1197
+ // Only a clean full-length timeout is replayed: a VFX stop ends early, and page or script
1198
+ // errors must reach each session so it can still classify the wait as a script failure.
1199
+ const cleanTimeout = session.subTimelineWaitOutcome === "timeout" &&
1200
+ session.vfxFailure === undefined &&
1201
+ session.pageErrors.length === 0 &&
1202
+ session.scriptLoadFailures.length === 0;
1203
+ if (cleanTimeout) {
1204
+ memo.unregisteredIds = session.pendingTimelineIds ?? [];
1205
+ }
1206
+ }
1207
+ export async function pollSubCompositionTimelines(page, timeoutMs, { intervalMs = 150, getScriptLoadFailures, scriptFailureGraceMs = 2_000, onPending, shouldStop, knownUnregisteredIds = [], } = {}) {
1182
1208
  // Hosts may opt out of the timeline wait with `data-no-timeline` —
1183
1209
  // compositions driven purely by CSS animations / rAF (the render-compat
1184
1210
  // contract) never register window.__timelines[id], and without the opt-out
@@ -1187,10 +1213,11 @@ shouldStop) {
1187
1213
  var hosts = document.querySelectorAll("[data-composition-id]");
1188
1214
  if (hosts.length === 0) return true;
1189
1215
  var timelines = window.__timelines || {};
1216
+ var known = ${JSON.stringify(knownUnregisteredIds)};
1190
1217
  for (var i = 0; i < hosts.length; i++) {
1191
1218
  if (hosts[i].hasAttribute("data-no-timeline")) continue;
1192
1219
  var id = hosts[i].getAttribute("data-composition-id");
1193
- if (!id) continue;
1220
+ if (!id || known.indexOf(id) !== -1) continue;
1194
1221
  if (!timelines[id]) return false;
1195
1222
  }
1196
1223
  return true;
@@ -1217,6 +1244,11 @@ shouldStop) {
1217
1244
  }
1218
1245
  await new Promise((resolve) => setTimeout(resolve, intervalMs));
1219
1246
  }
1247
+ // Skipped known ids still count: if one is still unregistered, report the timeout again.
1248
+ const pendingIds = ready && knownUnregisteredIds.length === 0 ? [] : await listUnregisteredTimelineIds(page);
1249
+ if (ready && pendingIds.length > 0 && (getScriptLoadFailures?.() ?? []).length > 0) {
1250
+ scriptFailureBail = true;
1251
+ }
1220
1252
  // Always force a timeline rebind once sub-composition timelines are
1221
1253
  // confirmed present. The previous implementation only called rebind
1222
1254
  // when the timeline count grew during the poll, which missed the case
@@ -1224,7 +1256,7 @@ shouldStop) {
1224
1256
  // started — leaving child timelines un-nested in the root and causing
1225
1257
  // the earliest sub-composition (data-start near 0) to render without
1226
1258
  // its GSAP animations.
1227
- if (ready) {
1259
+ if (ready && pendingIds.length === 0) {
1228
1260
  await page.evaluate(`(function() {
1229
1261
  if (typeof window.__hfForceTimelineRebind === "function") {
1230
1262
  window.__hfForceTimelineRebind();
@@ -1232,26 +1264,6 @@ shouldStop) {
1232
1264
  })()`);
1233
1265
  return "ready";
1234
1266
  }
1235
- // Enumerate the still-unregistered composition ids regardless of bail
1236
- // reason — a script-failure bail used to skip this entirely, so a render
1237
- // with multiple sub-compositions only named the failed script URL(s), not
1238
- // which composition(s) it was still waiting on (review).
1239
- const evaluated = await page.evaluate(`(function() {
1240
- var hosts = document.querySelectorAll("[data-composition-id]");
1241
- var timelines = window.__timelines || {};
1242
- var m = [];
1243
- for (var i = 0; i < hosts.length; i++) {
1244
- if (hosts[i].hasAttribute("data-no-timeline")) continue;
1245
- var id = hosts[i].getAttribute("data-composition-id");
1246
- if (id && !timelines[id]) m.push(id);
1247
- }
1248
- return m;
1249
- })()`);
1250
- // This block exists to BUILD A WARNING, so it must never be the thing that
1251
- // throws. `page.evaluate` is loosely typed, and a caller that stubs it (or a
1252
- // runtime that returns nothing here) would turn a blind `as string[]` cast
1253
- // into a TypeError on the diagnostic path. Normalise instead of asserting.
1254
- const pendingIds = Array.isArray(evaluated) ? evaluated.map((id) => String(id)) : [];
1255
1267
  onPending?.(pendingIds);
1256
1268
  const missing = pendingIds.join(", ");
1257
1269
  if (scriptFailureBail) {
@@ -1733,9 +1745,7 @@ export async function initializeSession(session) {
1733
1745
  const pageReadyTimeout = session.config?.playerReadyTimeout ?? DEFAULT_CONFIG.playerReadyTimeout;
1734
1746
  await pollHfReady(page, pageReadyTimeout);
1735
1747
  logInitPhase("pollHfReady complete");
1736
- session.subTimelineWaitOutcome = await pollSubCompositionTimelines(page, pageReadyTimeout, undefined, () => session.scriptLoadFailures, undefined, (ids) => {
1737
- session.pendingTimelineIds = [...ids];
1738
- }, () => session.vfxFailure !== undefined);
1748
+ await waitForSubCompositionTimelines(session, page, pageReadyTimeout);
1739
1749
  logInitPhase(`pollSubCompositionTimelines complete (${session.subTimelineWaitOutcome})`);
1740
1750
  assertVfxIntact(session);
1741
1751
  recordSubTimelineWarning(session, pageReadyTimeout);
@@ -1865,9 +1875,7 @@ export async function initializeSession(session) {
1865
1875
  const pageReadyTimeout = session.config?.playerReadyTimeout ?? DEFAULT_CONFIG.playerReadyTimeout;
1866
1876
  await pollHfReady(page, pageReadyTimeout);
1867
1877
  logInitPhase("pollHfReady complete");
1868
- session.subTimelineWaitOutcome = await pollSubCompositionTimelines(page, pageReadyTimeout, undefined, () => session.scriptLoadFailures, undefined, (ids) => {
1869
- session.pendingTimelineIds = [...ids];
1870
- }, () => session.vfxFailure !== undefined);
1878
+ await waitForSubCompositionTimelines(session, page, pageReadyTimeout);
1871
1879
  logInitPhase(`pollSubCompositionTimelines complete (${session.subTimelineWaitOutcome})`);
1872
1880
  assertVfxIntact(session);
1873
1881
  recordSubTimelineWarning(session, pageReadyTimeout);
@@ -2465,7 +2473,10 @@ export async function createStaticVerificationPage(session) {
2465
2473
  });
2466
2474
  await page.evaluate(`window.__hfFlushSync?.()`);
2467
2475
  await pollHfReady(page, pageReadyTimeout);
2468
- await pollSubCompositionTimelines(page, pageReadyTimeout);
2476
+ await pollSubCompositionTimelines(page, pageReadyTimeout, {
2477
+ getScriptLoadFailures: () => session.scriptLoadFailures,
2478
+ knownUnregisteredIds: session.subTimelineWaitMemo.unregisteredIds,
2479
+ });
2469
2480
  await applyVideoMetadataHints(page, session.options.videoMetadataHints);
2470
2481
  const skipVideoIds = session.options.skipReadinessVideoIds ?? [];
2471
2482
  await Promise.all([
@@ -3472,10 +3483,8 @@ export async function captureFramesBatchPipelined(session, frameIndices, times)
3472
3483
  * `captureFrameCore`.
3473
3484
  */
3474
3485
  export async function discardWarmupCapture(session, frameIndex = 0, time = 0, innerCapture = captureFrameCore) {
3475
- // Snapshot the side-effect counters captureFrameCore mutates. We use a
3476
- // shallow `{...}` for capturePerf because all five fields are primitive
3477
- // numbers — no nested state to deep-copy.
3478
- const perfBefore = { ...session.capturePerf };
3486
+ // The discarded capture mutates frameMs, so preserve its samples by value.
3487
+ const perfBefore = { ...session.capturePerf, frameMs: [...session.capturePerf.frameMs] };
3479
3488
  const hasDamageBefore = session.beginFrameHasDamageCount;
3480
3489
  const noDamageBefore = session.beginFrameNoDamageCount;
3481
3490
  const dedupCountBefore = session.staticDedupCount;