@@ -10234,10 +10234,33 @@ function HtmlViewer({
1023410234 probeId: string;
1023510235 recoverOnFailure: boolean;
1023610236 } | null>(null);
10237+ const srcDocTransportTimeoutsRef = useRef<Set<number>>(new Set());
1023710238 const srcDocParsingGraceRef = useRef<{
1023810239 generation: string;
1023910240 deadline: number;
1024010241 } | null>(null);
10242+ const clearSrcDocTransportTimeouts = useCallback(() => {
10243+ for (const timeout of srcDocTransportTimeoutsRef.current) {
10244+ window.clearTimeout(timeout);
10245+ }
10246+ srcDocTransportTimeoutsRef.current.clear();
10247+ }, []);
10248+ const scheduleSrcDocTransportTimeout = useCallback((callback: () => void, delay: number) => {
10249+ const timeout = window.setTimeout(() => {
10250+ srcDocTransportTimeoutsRef.current.delete(timeout);
10251+ callback();
10252+ }, delay);
10253+ srcDocTransportTimeoutsRef.current.add(timeout);
10254+ }, []);
10255+ const cancelPendingSrcDocTransport = useCallback(() => {
10256+ clearSrcDocTransportTimeouts();
10257+ pendingSrcDocTransportProbeRef.current = null;
10258+ srcDocParsingGraceRef.current = null;
10259+ }, [clearSrcDocTransportTimeouts]);
10260+ useEffect(() => {
10261+ if (!workspaceActive || mode !== 'preview') cancelPendingSrcDocTransport();
10262+ return cancelPendingSrcDocTransport;
10263+ }, [cancelPendingSrcDocTransport, mode, srcDocTransportGeneration, workspaceActive]);
1024110264 const replayPreviewBridgeModes = useCallback((target: HTMLIFrameElement | null) => {
1024210265 if (!workspaceActive) return;
1024310266 const win = target?.contentWindow;
@@ -10355,6 +10378,7 @@ function HtmlViewer({
1035510378 if (srcDocRecoveryAttemptedGenerationRef.current === generation) return;
1035610379 srcDocRecoveryAttemptedGenerationRef.current = generation;
1035710380 const ready = readySrcDocTransportRef.current;
10381+ cancelPendingSrcDocTransport();
1035810382 reportPreviewTransportRecovery({
1035910383 surface: 'artifact_preview',
1036010384 renderMode: 'srcdoc',
@@ -10371,15 +10395,13 @@ function HtmlViewer({
1037110395 viewportHeight: frame?.clientHeight,
1037210396 timeoutMs: signal === 'probe_timeout' ? SRC_DOC_READY_PROBE_TIMEOUT_MS : undefined,
1037310397 });
10374- pendingSrcDocTransportProbeRef.current = null;
10375- srcDocParsingGraceRef.current = null;
1037610398 verifiedSrcDocTransportRef.current = null;
1037710399 readySrcDocTransportRef.current = null;
1037810400 activatedSrcDocTransportHtmlRef.current = null;
1037910401 setSrcDocShellReady(false);
1038010402 setSrcDocRecoveryGeneration(generation);
1038110403 setSrcDocTransportResetKey((key) => key + 1);
10382- }, [file.kind, file.name, handoffArtifactKind, projectId]);
10404+ }, [cancelPendingSrcDocTransport, file.kind, file.name, handoffArtifactKind, projectId]);
1038310405 const probeSrcDocTransport = useCallback((
1038410406 generation: string,
1038510407 recoverOnFailure: boolean,
@@ -10401,6 +10423,7 @@ function HtmlViewer({
1040110423 // Recovery probes can be shared by the timer and onLoad paths. A passive
1040210424 // prewarm probe may target the lazy shell, so the real srcDoc must replace it.
1040310425 if (pendingRecoveryProbeMatches) return;
10426+ clearSrcDocTransportTimeouts();
1040410427 srcDocTransportProbeSequenceRef.current += 1;
1040510428 const probeId = `${generation}:probe-${srcDocTransportProbeSequenceRef.current}`;
1040610429 pendingSrcDocTransportProbeRef.current = {
@@ -10420,7 +10443,7 @@ function HtmlViewer({
1042010443 generation,
1042110444 probeId,
1042210445 }, '*');
10423- window.setTimeout (() => {
10446+ scheduleSrcDocTransportTimeout (() => {
1042410447 const pending = pendingSrcDocTransportProbeRef.current;
1042510448 if (
1042610449 !pending
@@ -10435,7 +10458,11 @@ function HtmlViewer({
1043510458 recoverUnacknowledgedSrcDocTransport(generation, 'probe_timeout');
1043610459 }
1043710460 }, SRC_DOC_READY_PROBE_TIMEOUT_MS);
10438- }, [recoverUnacknowledgedSrcDocTransport]);
10461+ }, [
10462+ clearSrcDocTransportTimeouts,
10463+ recoverUnacknowledgedSrcDocTransport,
10464+ scheduleSrcDocTransportTimeout,
10465+ ]);
1043910466 // Sticky once the srcDoc iframe has materialized the real artifact for the
1044010467 // first time (i.e. the first entry into Mark/Edit/Comment/Inspect). Until
1044110468 // then the srcDoc iframe stays on the lazy shell — so passive preview never
@@ -10517,6 +10544,7 @@ function HtmlViewer({
1051710544 && pending.probeId === data.probeId
1051810545 && data.bodyComplete === true
1051910546 ) {
10547+ clearSrcDocTransportTimeouts();
1052010548 pendingSrcDocTransportProbeRef.current = null;
1052110549 srcDocParsingGraceRef.current = null;
1052210550 verifiedSrcDocTransportRef.current = { frame, generation: data.generation };
@@ -10534,6 +10562,7 @@ function HtmlViewer({
1053410562 // so keep challenging this generation without remounting and risk
1053510563 // running earlier authored side effects twice. A bounded grace
1053610564 // period still recovers Chromium's permanently-aborted half-document.
10565+ clearSrcDocTransportTimeouts();
1053710566 pendingSrcDocTransportProbeRef.current = null;
1053810567 const now = Date.now();
1053910568 const currentGrace = srcDocParsingGraceRef.current;
@@ -10556,7 +10585,7 @@ function HtmlViewer({
1055610585 });
1055710586 return;
1055810587 }
10559- window.setTimeout (() => {
10588+ scheduleSrcDocTransportTimeout (() => {
1056010589 if (expectedSrcDocTransportGenerationRef.current !== data.generation) return;
1056110590 const verified = verifiedSrcDocTransportRef.current;
1056210591 if (verified?.frame === frame && verified.generation === data.generation) return;
@@ -10593,9 +10622,11 @@ function HtmlViewer({
1059310622 window.addEventListener('message', onMessage);
1059410623 return () => window.removeEventListener('message', onMessage);
1059510624 }, [
10625+ clearSrcDocTransportTimeouts,
1059610626 probeSrcDocTransport,
1059710627 recoverUnacknowledgedSrcDocTransport,
1059810628 replayPreviewBridgeModes,
10629+ scheduleSrcDocTransportTimeout,
1059910630 workspaceActive,
1060010631 ]);
1060110632 // React can commit a fresh `srcdoc` attribute while Chromium aborts the
0 commit comments