@@ -25,6 +25,7 @@ import type {
2525 DesktopPreviewScreenshotArtifact ,
2626 DesktopPreviewTabDefaults ,
2727 PreviewForwardedShortcut ,
28+ PreviewAutomationLoadError ,
2829} from "@t3tools/contracts" ;
2930import * as HostProcess from "@t3tools/shared/HostProcess" ;
3031import { matchesKeybindingShortcut } from "@t3tools/shared/keybindings" ;
@@ -603,15 +604,8 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function
603604 { readonly semaphore : Semaphore . Semaphore ; users : number }
604605 > ( ) ;
605606 const tabLifecycleGenerations = new Map < string , number > ( ) ;
606- /**
607- * The main-frame failure of each guest's current load, by webContents id.
608- * Watched from attach, because a guest starts loading before registerWebview
609- * installs its tab's listeners.
610- */
611- const guestLoadFailures = new Map <
612- number ,
613- { readonly url : string ; readonly code : number ; readonly description : string }
614- > ( ) ;
607+ // A guest can fail its first load before registerWebview installs its tab listeners.
608+ const guestLoadFailures = new Map < number , PreviewAutomationLoadError > ( ) ;
615609
616610 const attempt = < A > ( errorContext : PreviewOperationContext , evaluate : ( ) => A ) =>
617611 Effect . try ( {
@@ -1460,23 +1454,40 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function
14601454 validatedUrl : string ,
14611455 isMainFrame : boolean ,
14621456 ) : void => {
1463- if ( code === - 3 || ! isMainFrame ) return ;
1457+ if ( code === - 3 || ! isMainFrame || wc . isDestroyed ( ) ) return ;
14641458 const url = validatedUrl || wc . getURL ( ) ;
14651459 runFork (
1466- update ( tabId , {
1467- navStatus : { kind : "LoadFailed" , url, title : wc . getTitle ( ) , code, description } ,
1468- } ) . pipe (
1469- Effect . andThen ( SynchronizedRef . get ( tabsRef ) ) ,
1470- Effect . tap ( ( tabs ) =>
1471- Effect . sync ( ( ) => {
1472- const tab = tabs . get ( tabId ) ;
1473- // A replaced guest's failure is not the current page's.
1474- if ( tab ?. serverTab && tab . webContentsId === wc . id ) {
1475- browserHost . loadFailed ( tab . serverTab , { url, code, description } ) ;
1476- }
1477- } ) ,
1478- ) ,
1479- ) ,
1460+ Effect . gen ( function * ( ) {
1461+ const updatedAt = yield * currentIso ;
1462+ const next = yield * SynchronizedRef . modify ( tabsRef , ( tabs ) => {
1463+ const current = tabs . get ( tabId ) ;
1464+ if (
1465+ ! current ||
1466+ current . webContentsId !== wc . id ||
1467+ wc . isDestroyed ( ) ||
1468+ webContents . fromId ( wc . id ) !== wc
1469+ ) {
1470+ return [ Option . none < PreviewTabState > ( ) , tabs ] as const ;
1471+ }
1472+ const state : PreviewTabState = {
1473+ ...current ,
1474+ navStatus : { kind : "LoadFailed" , url, title : wc . getTitle ( ) , code, description } ,
1475+ updatedAt,
1476+ } ;
1477+ return [
1478+ Option . some ( state ) ,
1479+ replaceMap ( tabs , ( copy ) => copy . set ( tabId , state ) ) ,
1480+ ] as const ;
1481+ } ) ;
1482+ if ( Option . isNone ( next ) ) return ;
1483+ const current = ( yield * SynchronizedRef . get ( tabsRef ) ) . get ( tabId ) ;
1484+ if ( current ?. webContentsId !== wc . id || current . navStatus !== next . value . navStatus )
1485+ return ;
1486+ if ( current . serverTab ) {
1487+ browserHost . loadFailed ( current . serverTab , { url, code, description } ) ;
1488+ }
1489+ yield * emitIfCurrent ( tabId , next . value ) ;
1490+ } ) ,
14801491 ) ;
14811492 } ;
14821493 const handleHumanInput = Effect . fn ( "PreviewManager.handleHumanInput" ) ( function * (
@@ -2004,7 +2015,9 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function
20042015 ) {
20052016 yield * rendererHistory . register ( wc , { surface : "preview" } ) ;
20062017 const webContentsId = wc . id ;
2007- wc . on ( "did-start-loading" , ( ) => guestLoadFailures . delete ( webContentsId ) ) ;
2018+ wc . on ( "did-start-navigation" , ( event ) => {
2019+ if ( event . isMainFrame && ! event . isSameDocument ) guestLoadFailures . delete ( webContentsId ) ;
2020+ } ) ;
20082021 wc . on ( "did-fail-load" , ( _event , code , description , validatedUrl , isMainFrame ) => {
20092022 if ( code === - 3 || ! isMainFrame ) return ;
20102023 guestLoadFailures . set ( webContentsId , {
@@ -2411,15 +2424,15 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function
24112424 const beforeAttach = ( yield * SynchronizedRef . get ( tabsRef ) ) . get ( tabId ) ;
24122425 if ( beforeAttach ?. webContentsId !== wc . id ) return ;
24132426 const control = yield * ensureControlSession ( wc ) ;
2427+ if ( beforeAttach . serverTab ) yield * listenForAgentPointers ;
24142428 const afterAttach = ( yield * SynchronizedRef . get ( tabsRef ) ) . get ( tabId ) ;
24152429 if ( afterAttach ?. webContentsId !== wc . id ) {
24162430 yield * detachControlSession ( wc . id ) ;
24172431 return ;
24182432 }
24192433 if ( afterAttach . serverTab ) {
2420- yield * listenForAgentPointers ;
24212434 browserHost . attach ( afterAttach . serverTab , { webContents : wc , debugger : control . debugger } ) ;
2422- // A first load can fail before the attach, while no server is listening .
2435+ // CDP cannot recover a failure that happened before it attached .
24232436 const { navStatus } = afterAttach ;
24242437 if ( navStatus . kind === "LoadFailed" ) {
24252438 browserHost . loadFailed ( afterAttach . serverTab , {
0 commit comments