2121 cache.invalidate() # Force refresh on next get()
2222"""
2323
24+ import asyncio
2425import time
2526from typing import TYPE_CHECKING , Any
2627
3738 from .protocol import BrowserBackend
3839
3940
41+ def _is_execution_context_destroyed_error (e : Exception ) -> bool :
42+ """
43+ Playwright (and other browser backends) can throw while a navigation is in-flight.
44+
45+ Common symptoms:
46+ - "Execution context was destroyed, most likely because of a navigation"
47+ - "Cannot find context with specified id"
48+ """
49+ msg = str (e ).lower ()
50+ return (
51+ "execution context was destroyed" in msg
52+ or "most likely because of a navigation" in msg
53+ or "cannot find context with specified id" in msg
54+ )
55+
56+
57+ async def _eval_with_navigation_retry (
58+ backend : "BrowserBackend" ,
59+ expression : str ,
60+ * ,
61+ retries : int = 10 ,
62+ settle_state : str = "interactive" ,
63+ settle_timeout_ms : int = 10000 ,
64+ ) -> Any :
65+ """
66+ Evaluate JS, retrying once/ twice if the page is mid-navigation.
67+
68+ This makes snapshots resilient to cases like:
69+ - press Enter (navigation) → snapshot immediately → context destroyed
70+ """
71+ last_err : Exception | None = None
72+ for attempt in range (retries + 1 ):
73+ try :
74+ return await backend .eval (expression )
75+ except Exception as e :
76+ last_err = e
77+ if not _is_execution_context_destroyed_error (e ) or attempt >= retries :
78+ raise
79+ # Navigation is in-flight; wait for new document context then retry.
80+ try :
81+ await backend .wait_ready_state (state = settle_state , timeout_ms = settle_timeout_ms ) # type: ignore[arg-type]
82+ except Exception :
83+ # If readyState polling also fails mid-nav, still retry after a short backoff.
84+ pass
85+ # Exponential-ish backoff (caps quickly), tuned for real navigations.
86+ await asyncio .sleep (min (0.25 * (attempt + 1 ), 1.5 ))
87+
88+ # Unreachable in practice, but keeps type-checkers happy.
89+ raise last_err if last_err else RuntimeError ("eval failed" )
90+
91+
4092class CachedSnapshot :
4193 """
4294 Snapshot cache with staleness detection.
@@ -289,13 +341,14 @@ async def _snapshot_via_extension(
289341 ext_options = _build_extension_options (options )
290342
291343 # Call extension's snapshot function
292- result = await backend .eval (
344+ result = await _eval_with_navigation_retry (
345+ backend ,
293346 f"""
294347 (() => {{
295348 const options = { _json_serialize (ext_options )} ;
296349 return window.sentience.snapshot(options);
297350 }})()
298- """
351+ """ ,
299352 )
300353
301354 if result is None :
@@ -310,14 +363,15 @@ async def _snapshot_via_extension(
310363 if options .show_overlay :
311364 raw_elements = result .get ("raw_elements" , [])
312365 if raw_elements :
313- await backend .eval (
366+ await _eval_with_navigation_retry (
367+ backend ,
314368 f"""
315369 (() => {{
316370 if (window.sentience && window.sentience.showOverlay) {{
317371 window.sentience.showOverlay({ _json_serialize (raw_elements )} , null);
318372 }}
319373 }})()
320- """
374+ """ ,
321375 )
322376
323377 # Build and return Snapshot
@@ -341,13 +395,14 @@ async def _snapshot_via_api(
341395 raw_options ["screenshot" ] = options .screenshot
342396
343397 # Call extension to get raw elements
344- raw_result = await backend .eval (
398+ raw_result = await _eval_with_navigation_retry (
399+ backend ,
345400 f"""
346401 (() => {{
347402 const options = { _json_serialize (raw_options )} ;
348403 return window.sentience.snapshot(options);
349404 }})()
350- """
405+ """ ,
351406 )
352407
353408 if raw_result is None :
@@ -372,14 +427,15 @@ async def _snapshot_via_api(
372427 if options .show_overlay :
373428 elements = api_result .get ("elements" , [])
374429 if elements :
375- await backend .eval (
430+ await _eval_with_navigation_retry (
431+ backend ,
376432 f"""
377433 (() => {{
378434 if (window.sentience && window.sentience.showOverlay) {{
379435 window.sentience.showOverlay({ _json_serialize (elements )} , null);
380436 }}
381437 }})()
382- """
438+ """ ,
383439 )
384440
385441 return Snapshot (** snapshot_data )
0 commit comments