fix: multi-tab concurrency and metadata sync improvements
This commit is contained in:
@@ -14,6 +14,8 @@ node_modules/
|
||||
dist/
|
||||
build/
|
||||
.vite/
|
||||
frontend/auto-imports.d.ts
|
||||
frontend/components.d.ts
|
||||
|
||||
backend/static/
|
||||
backend/data/
|
||||
|
||||
@@ -141,6 +141,8 @@ class BrowserSessionService:
|
||||
self._sessions[session.id] = session
|
||||
self._profiles[profile_key] = session.id
|
||||
self._touch(session.id)
|
||||
# Register listeners for the initial tab
|
||||
self._setup_tab_listeners(session, page)
|
||||
# Register page capture for multi-tab support
|
||||
context.on("page", lambda p: self._handle_new_page(session, p))
|
||||
# Evict again after adding the new session so cap is enforced immediately
|
||||
@@ -160,14 +162,31 @@ class BrowserSessionService:
|
||||
|
||||
def _handle_new_page(self, session: BrowserSession, page: Any) -> None:
|
||||
"""Capture a new page opened by the remote browser (e.g. target="_blank")."""
|
||||
asyncio.create_task(self._register_new_page(session, page))
|
||||
|
||||
def _setup_tab_listeners(self, session: BrowserSession, page: Any) -> None:
|
||||
"""Register navigation and state listeners to bump tab_revision."""
|
||||
def bump_revision(_=None):
|
||||
session.tab_revision += 1
|
||||
|
||||
page.on("domcontentloaded", bump_revision)
|
||||
page.on("load", bump_revision)
|
||||
page.on("framenavigated", bump_revision)
|
||||
page.on("close", bump_revision)
|
||||
|
||||
async def _register_new_page(self, session: BrowserSession, page: Any) -> None:
|
||||
tab_id = uuid4().hex
|
||||
tab = BrowserTab(id=tab_id, page=page, created_at=asyncio.get_event_loop().time())
|
||||
session.tabs[tab_id] = tab
|
||||
session.active_tab_id = tab_id
|
||||
session.tab_revision += 1
|
||||
logger.info("session %s: captured new tab %s (total: %d)", session.id[:12], tab_id[:8], len(session.tabs))
|
||||
|
||||
async with session.lock:
|
||||
session.tabs[tab_id] = tab
|
||||
session.active_tab_id = tab_id
|
||||
session.tab_revision += 1
|
||||
logger.info("session %s: captured new tab %s (total: %d)", session.id[:12], tab_id[:8], len(session.tabs))
|
||||
|
||||
self._setup_tab_listeners(session, page)
|
||||
# Best-effort: bring to front and reset zoom
|
||||
asyncio.create_task(self._init_new_tab(session, tab))
|
||||
await self._init_new_tab(session, tab)
|
||||
|
||||
async def _init_new_tab(self, session: BrowserSession, tab: BrowserTab) -> None:
|
||||
try:
|
||||
@@ -399,7 +418,8 @@ class BrowserSessionService:
|
||||
tabs = []
|
||||
# We might need to prune closed pages during state generation too
|
||||
closed_ids = []
|
||||
for tid, tab in session.tabs.items():
|
||||
# Use list() to avoid RuntimeError if tabs dict changes during iteration
|
||||
for tid, tab in list(session.tabs.items()):
|
||||
if tab.page.is_closed():
|
||||
closed_ids.append(tid)
|
||||
continue
|
||||
@@ -734,6 +754,8 @@ class BrowserSessionService:
|
||||
)
|
||||
self._sessions[session.id] = session
|
||||
self._touch(session.id)
|
||||
# Register listeners for the initial tab
|
||||
self._setup_tab_listeners(session, page)
|
||||
# Register page capture
|
||||
context.on("page", lambda p: self._handle_new_page(session, p))
|
||||
# Start CDP network capture BEFORE the initial page load,
|
||||
|
||||
@@ -12,6 +12,8 @@ def service():
|
||||
def session(service):
|
||||
fake_context = AsyncMock()
|
||||
fake_page = AsyncMock()
|
||||
# Playwright's page.on is synchronous
|
||||
fake_page.on = MagicMock()
|
||||
fake_page.is_closed = MagicMock(return_value=False)
|
||||
fake_page.url = "https://initial.test"
|
||||
fake_page.title = AsyncMock(return_value="Initial Tab")
|
||||
@@ -34,6 +36,7 @@ def session(service):
|
||||
@pytest.mark.asyncio
|
||||
async def test_tab_capture(service, session):
|
||||
new_page = AsyncMock()
|
||||
new_page.on = MagicMock()
|
||||
new_page.is_closed = MagicMock(return_value=False)
|
||||
new_page.url = "https://new.test"
|
||||
new_page.title = AsyncMock(return_value="New Tab")
|
||||
@@ -42,6 +45,9 @@ async def test_tab_capture(service, session):
|
||||
# Simulate page capture
|
||||
service._handle_new_page(session, new_page)
|
||||
|
||||
# Wait for the background registration task to finish
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
assert len(session.tabs) == 2
|
||||
assert session.active_tab_id != "tab1"
|
||||
new_tab_id = session.active_tab_id
|
||||
|
||||
Reference in New Issue
Block a user