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