refactor tab group sync to be reactive and handle edge cases

This commit is contained in:
Tommy D. Rossi
2025-12-19 19:22:36 +01:00
parent 5b474087eb
commit f97052f4f6
+73 -38
View File
@@ -9,6 +9,7 @@ let ws: WebSocket | null = null
let childSessions: Map<string, number> = new Map() let childSessions: Map<string, number> = new Map()
let nextSessionId = 1 let nextSessionId = 1
let playwriterGroupId: number | null = null let playwriterGroupId: number | null = null
let syncTabGroupQueue: Promise<void> = Promise.resolve()
const store = createStore<ExtensionState>(() => ({ const store = createStore<ExtensionState>(() => ({
tabs: new Map(), tabs: new Map(),
@@ -95,43 +96,72 @@ function sendMessage(message: any): void {
} }
} }
async function addTabToGroup(tabId: number): Promise<void> { async function syncTabGroup(): Promise<void> {
try { try {
if (playwriterGroupId !== null) { const connectedTabIds = Array.from(store.getState().tabs.entries())
const existingGroups = await chrome.tabGroups.query({ title: 'playwriter' }) .filter(([_, info]) => info.state === 'connected')
if (!existingGroups.some((g) => g.id === playwriterGroupId)) { .map(([tabId]) => tabId)
playwriterGroupId = null
const existingGroups = await chrome.tabGroups.query({ title: 'playwriter' })
if (existingGroups.length > 1) {
const [keep, ...duplicates] = existingGroups
for (const group of duplicates) {
const tabsInDupe = await chrome.tabs.query({ groupId: group.id })
for (const tab of tabsInDupe) {
if (tab.id) {
await chrome.tabs.ungroup(tab.id)
}
}
logger.debug('Removed duplicate playwriter group:', group.id)
}
playwriterGroupId = keep.id
} else if (existingGroups.length === 1) {
playwriterGroupId = existingGroups[0].id
}
if (connectedTabIds.length === 0) {
for (const group of existingGroups) {
const tabsInGroup = await chrome.tabs.query({ groupId: group.id })
for (const tab of tabsInGroup) {
if (tab.id) {
await chrome.tabs.ungroup(tab.id)
}
}
logger.debug('Cleared playwriter group:', group.id)
}
playwriterGroupId = null
return
}
const allTabs = await chrome.tabs.query({})
const tabsInGroup = allTabs.filter((t) => t.groupId === playwriterGroupId && t.id !== undefined)
const tabIdsInGroup = new Set(tabsInGroup.map((t) => t.id!))
const tabsToAdd = connectedTabIds.filter((id) => !tabIdsInGroup.has(id))
const tabsToRemove = Array.from(tabIdsInGroup).filter((id) => !connectedTabIds.includes(id))
for (const tabId of tabsToRemove) {
try {
await chrome.tabs.ungroup(tabId)
logger.debug('Removed tab from group:', tabId)
} catch (e: any) {
logger.debug('Failed to ungroup tab:', tabId, e.message)
} }
} }
if (playwriterGroupId === null) { if (tabsToAdd.length > 0) {
playwriterGroupId = await chrome.tabs.group({ tabIds: [tabId] }) if (playwriterGroupId === null) {
await chrome.tabGroups.update(playwriterGroupId, { title: 'playwriter', color: 'green' }) playwriterGroupId = await chrome.tabs.group({ tabIds: tabsToAdd })
logger.debug('Created tab group:', playwriterGroupId) await chrome.tabGroups.update(playwriterGroupId, { title: 'playwriter', color: 'green' })
} else { logger.debug('Created tab group:', playwriterGroupId, 'with tabs:', tabsToAdd)
await chrome.tabs.group({ tabIds: [tabId], groupId: playwriterGroupId }) } else {
logger.debug('Added tab to existing group:', tabId) await chrome.tabs.group({ tabIds: tabsToAdd, groupId: playwriterGroupId })
} logger.debug('Added tabs to existing group:', tabsToAdd)
} catch (error: any) {
logger.debug('Failed to add tab to group:', error.message)
}
}
async function removeTabFromGroup(tabId: number): Promise<void> {
try {
await chrome.tabs.ungroup(tabId)
logger.debug('Removed tab from group:', tabId)
if (playwriterGroupId !== null) {
const groups = await chrome.tabGroups.query({ title: 'playwriter' })
const group = groups.find((g) => g.id === playwriterGroupId)
if (!group) {
playwriterGroupId = null
logger.debug('Tab group was deleted')
} }
} }
} catch (error: any) { } catch (error: any) {
logger.debug('Failed to remove tab from group:', error.message) logger.debug('Failed to sync tab group:', error.message)
} }
} }
@@ -340,7 +370,6 @@ async function attachTab(tabId: number): Promise<Protocol.Target.TargetInfo> {
}) })
logger.debug('Tab attached successfully:', tabId, 'sessionId:', sessionId, 'targetId:', targetInfo.targetId) logger.debug('Tab attached successfully:', tabId, 'sessionId:', sessionId, 'targetId:', targetInfo.targetId)
await addTabToGroup(tabId)
return targetInfo return targetInfo
} }
@@ -353,8 +382,6 @@ function detachTab(tabId: number, shouldDetachDebugger: boolean): void {
logger.debug('Detaching tab:', tabId, 'sessionId:', tab.sessionId, 'shouldDetach:', shouldDetachDebugger) logger.debug('Detaching tab:', tabId, 'sessionId:', tab.sessionId, 'shouldDetach:', shouldDetachDebugger)
void removeTabFromGroup(tabId)
sendMessage({ sendMessage({
method: 'forwardCDPEvent', method: 'forwardCDPEvent',
params: { params: {
@@ -397,7 +424,6 @@ function closeConnection(reason: string): void {
store.setState({ tabs: new Map(), connectionState: 'disconnected', errorText: undefined }) store.setState({ tabs: new Map(), connectionState: 'disconnected', errorText: undefined })
childSessions.clear() childSessions.clear()
playwriterGroupId = null
if (ws) { if (ws) {
ws.close(1000, reason) ws.close(1000, reason)
@@ -420,7 +446,6 @@ function handleConnectionClose(reason: string, code: number): void {
} }
childSessions.clear() childSessions.clear()
playwriterGroupId = null
ws = null ws = null
if (reason === 'Extension Replaced' || code === 4001) { if (reason === 'Extension Replaced' || code === 4001) {
@@ -851,9 +876,19 @@ chrome.runtime.onInstalled.addListener((details) => {
} }
}) })
store.subscribe(async () => { function serializeTabs(tabs: Map<number, TabInfo>): string {
logger.log(store.getState()) return JSON.stringify(Array.from(tabs.entries()))
await updateIcons() }
store.subscribe((state, prevState) => {
logger.log(state)
void updateIcons()
const tabsChanged = serializeTabs(state.tabs) !== serializeTabs(prevState.tabs)
if (tabsChanged) {
syncTabGroupQueue = syncTabGroupQueue.then(syncTabGroup).catch((e) => {
logger.debug('syncTabGroup error:', e)
})
}
}) })
logger.debug(`Using relay URL: ${RELAY_URL}`) logger.debug(`Using relay URL: ${RELAY_URL}`)