@@ -40,8 +40,11 @@ import { deployErrorCode } from '../analytics/deploy-error-code';
4040import { publishErrorCode } from '../analytics/publish-error-code';
4141import {
4242 reportPreviewIframeMessage,
43+ reportPreviewTransportRecovery,
4344 subscribePreviewIframeMessages,
4445 trackIframeLoad,
46+ type PreviewTransportDocumentState,
47+ type PreviewTransportRecoverySignal,
4548} from '../observability/iframe-error';
4649import { notifyExportSucceeded } from './experience-survey-trigger';
4750import {
@@ -621,6 +624,8 @@ const MAX_CACHED_PREVIEW_CONTENT_WIDTHS = 128;
621624const PREVIEW_CONTENT_WIDTH_CACHE_VERSION = 2;
622625const SRC_DOC_ACTIVATION_RECOVERY_TIMEOUT_MS = 1500;
623626const SRC_DOC_READY_PROBE_TIMEOUT_MS = 1500;
627+ const SRC_DOC_PARSING_RECHECK_MS = 1500;
628+ const SRC_DOC_PARSING_COMPLETION_TIMEOUT_MS = 10_000;
624629let previewContentMeasurementDocumentEpochSequence = 0;
625630let previewContentMeasurementHostInstanceSequence = 0;
626631let previewTransportGenerationSequence = 0;
@@ -10212,6 +10217,10 @@ function HtmlViewer({
1021210217 probeId: string;
1021310218 recoverOnFailure: boolean;
1021410219 } | null>(null);
10220+ const srcDocParsingGraceRef = useRef<{
10221+ generation: string;
10222+ deadline: number;
10223+ } | null>(null);
1021510224 const replayPreviewBridgeModes = useCallback((target: HTMLIFrameElement | null) => {
1021610225 if (!workspaceActive) return;
1021710226 const win = target?.contentWindow;
@@ -10312,7 +10321,11 @@ function HtmlViewer({
1031210321 const [srcDocShellReady, setSrcDocShellReady] = useState(false);
1031310322 const srcDocRecoveryAttemptedGenerationRef = useRef<string | null>(null);
1031410323 const [srcDocRecoveryGeneration, setSrcDocRecoveryGeneration] = useState<string | null>(null);
10315- const recoverUnacknowledgedSrcDocTransport = useCallback((generation: string) => {
10324+ const recoverUnacknowledgedSrcDocTransport = useCallback((
10325+ generation: string,
10326+ signal: PreviewTransportRecoverySignal,
10327+ documentState?: PreviewTransportDocumentState,
10328+ ) => {
1031610329 if (
1031710330 !workspaceActiveRef.current
1031810331 || expectedSrcDocTransportGenerationRef.current !== generation
@@ -10324,14 +10337,32 @@ function HtmlViewer({
1032410337 if (frame && verified?.frame === frame && verified.generation === generation) return;
1032510338 if (srcDocRecoveryAttemptedGenerationRef.current === generation) return;
1032610339 srcDocRecoveryAttemptedGenerationRef.current = generation;
10340+ const ready = readySrcDocTransportRef.current;
10341+ reportPreviewTransportRecovery({
10342+ surface: 'artifact_preview',
10343+ renderMode: 'srcdoc',
10344+ artifactId: anonymizeArtifactId({ projectId, fileName: file.name }),
10345+ artifactKind:
10346+ handoffArtifactKind
10347+ ?? artifactKindToTracking({ fileKind: file.kind ?? null }),
10348+ projectId,
10349+ signal,
10350+ activationAcknowledged:
10351+ ready?.frame === frame && ready.generation === generation,
10352+ documentState,
10353+ viewportWidth: frame?.clientWidth,
10354+ viewportHeight: frame?.clientHeight,
10355+ timeoutMs: signal === 'probe_timeout' ? SRC_DOC_READY_PROBE_TIMEOUT_MS : undefined,
10356+ });
1032710357 pendingSrcDocTransportProbeRef.current = null;
10358+ srcDocParsingGraceRef.current = null;
1032810359 verifiedSrcDocTransportRef.current = null;
1032910360 readySrcDocTransportRef.current = null;
1033010361 activatedSrcDocTransportHtmlRef.current = null;
1033110362 setSrcDocShellReady(false);
1033210363 setSrcDocRecoveryGeneration(generation);
1033310364 setSrcDocTransportResetKey((key) => key + 1);
10334- }, []);
10365+ }, [file.kind, file.name, handoffArtifactKind, projectId ]);
1033510366 const probeSrcDocTransport = useCallback((
1033610367 generation: string,
1033710368 recoverOnFailure: boolean,
@@ -10363,8 +10394,9 @@ function HtmlViewer({
1036310394 };
1036410395 // An eager acknowledgement from the injected head bridge is provisional:
1036510396 // Chromium can still abort the about:srcdoc navigation after it was sent.
10366- // Only this exact challenge response proves the current browsing context is
10367- // alive after the navigation had a chance to commit.
10397+ // Only this exact challenge response, together with the body-end witness,
10398+ // proves the current browsing context is alive and fully parsed after the
10399+ // navigation had a chance to commit.
1036810400 verifiedSrcDocTransportRef.current = null;
1036910401 frame.contentWindow?.postMessage({
1037010402 type: 'od:srcdoc-transport-ready-probe',
@@ -10382,7 +10414,9 @@ function HtmlViewer({
1038210414 return;
1038310415 }
1038410416 pendingSrcDocTransportProbeRef.current = null;
10385- if (pending.recoverOnFailure) recoverUnacknowledgedSrcDocTransport(generation);
10417+ if (pending.recoverOnFailure) {
10418+ recoverUnacknowledgedSrcDocTransport(generation, 'probe_timeout');
10419+ }
1038610420 }, SRC_DOC_READY_PROBE_TIMEOUT_MS);
1038710421 }, [recoverUnacknowledgedSrcDocTransport]);
1038810422 // Sticky once the srcDoc iframe has materialized the real artifact for the
@@ -10443,6 +10477,11 @@ function HtmlViewer({
1044310477 type?: unknown;
1044410478 generation?: unknown;
1044510479 probeId?: unknown;
10480+ bodyComplete?: unknown;
10481+ documentReadyState?: unknown;
10482+ bodyPresent?: unknown;
10483+ bodyChildCount?: unknown;
10484+ documentElementChildCount?: unknown;
1044610485 } | null;
1044710486 const pending = pendingSrcDocTransportProbeRef.current;
1044810487 if (
@@ -10459,21 +10498,96 @@ function HtmlViewer({
1045910498 && pending.frame === frame
1046010499 && pending.generation === data.generation
1046110500 && pending.probeId === data.probeId
10501+ && data.bodyComplete === true
1046210502 ) {
1046310503 pendingSrcDocTransportProbeRef.current = null;
10504+ srcDocParsingGraceRef.current = null;
1046410505 verifiedSrcDocTransportRef.current = { frame, generation: data.generation };
10506+ } else if (
10507+ typeof data.probeId === 'string'
10508+ && pending
10509+ && pending.frame === frame
10510+ && pending.generation === data.generation
10511+ && pending.probeId === data.probeId
10512+ && pending.recoverOnFailure
10513+ ) {
10514+ if (data.documentReadyState === 'loading') {
10515+ // A healthy parser can remain in `loading` while it waits on an
10516+ // authored parser-blocking resource. The head bridge is responsive,
10517+ // so keep challenging this generation without remounting and risk
10518+ // running earlier authored side effects twice. A bounded grace
10519+ // period still recovers Chromium's permanently-aborted half-document.
10520+ pendingSrcDocTransportProbeRef.current = null;
10521+ const now = Date.now();
10522+ const currentGrace = srcDocParsingGraceRef.current;
10523+ const grace = currentGrace?.generation === data.generation
10524+ ? currentGrace
10525+ : {
10526+ generation: data.generation,
10527+ deadline: now + SRC_DOC_PARSING_COMPLETION_TIMEOUT_MS,
10528+ };
10529+ srcDocParsingGraceRef.current = grace;
10530+ if (now >= grace.deadline) {
10531+ recoverUnacknowledgedSrcDocTransport(data.generation, 'body_incomplete', {
10532+ readyState: 'loading',
10533+ bodyPresent: typeof data.bodyPresent === 'boolean' ? data.bodyPresent : undefined,
10534+ bodyChildCount: typeof data.bodyChildCount === 'number' ? data.bodyChildCount : undefined,
10535+ documentElementChildCount:
10536+ typeof data.documentElementChildCount === 'number'
10537+ ? data.documentElementChildCount
10538+ : undefined,
10539+ });
10540+ return;
10541+ }
10542+ window.setTimeout(() => {
10543+ if (expectedSrcDocTransportGenerationRef.current !== data.generation) return;
10544+ const verified = verifiedSrcDocTransportRef.current;
10545+ if (verified?.frame === frame && verified.generation === data.generation) return;
10546+ probeSrcDocTransport(data.generation, true);
10547+ }, Math.min(SRC_DOC_PARSING_RECHECK_MS, grace.deadline - now));
10548+ return;
10549+ }
10550+ // The exact challenged head bridge answered, but it could not observe
10551+ // the inert marker placed after all authored body content. This is the
10552+ // characteristic half-document state from an aborted about:srcdoc;
10553+ // recover immediately instead of waiting for the probe timeout.
10554+ recoverUnacknowledgedSrcDocTransport(data.generation, 'body_incomplete', {
10555+ readyState:
10556+ typeof data.documentReadyState === 'string'
10557+ ? data.documentReadyState
10558+ : undefined,
10559+ bodyPresent:
10560+ typeof data.bodyPresent === 'boolean'
10561+ ? data.bodyPresent
10562+ : undefined,
10563+ bodyChildCount:
10564+ typeof data.bodyChildCount === 'number'
10565+ ? data.bodyChildCount
10566+ : undefined,
10567+ documentElementChildCount:
10568+ typeof data.documentElementChildCount === 'number'
10569+ ? data.documentElementChildCount
10570+ : undefined,
10571+ });
10572+ return;
1046510573 }
1046610574 if (frame === iframeRef.current) replayPreviewBridgeModes(frame);
1046710575 }
1046810576 window.addEventListener('message', onMessage);
1046910577 return () => window.removeEventListener('message', onMessage);
10470- }, [replayPreviewBridgeModes, workspaceActive]);
10578+ }, [
10579+ probeSrcDocTransport,
10580+ recoverUnacknowledgedSrcDocTransport,
10581+ replayPreviewBridgeModes,
10582+ workspaceActive,
10583+ ]);
1047110584 // React can commit a fresh `srcdoc` attribute while Chromium aborts the
1047210585 // corresponding about:srcdoc navigation. The injected head bridge may run
1047310586 // and announce eagerly before that abort, so a plain generation ACK is not a
1047410587 // committed-document witness. Challenge the current browsing context after
10475- // the navigation had time to settle and require the exact probe token back;
10476- // otherwise retry through the small lazy shell automatically. Chromium can
10588+ // the navigation had time to settle and require both the exact probe token
10589+ // and an inert body-end marker; otherwise retry through the small lazy shell
10590+ // automatically. Chromium can
1047710591 // commit that shell even when it aborts a large direct srcDoc navigation,
1047810592 // after which the existing ready handshake safely document.write's the
1047910593 // latest HTML. One fallback per generation avoids a loop when an authored
0 commit comments