use zustand for background.ts and update icons on state updates only.
less imperative code
This commit is contained in:
@@ -5,13 +5,14 @@ import { create } from 'zustand';
|
|||||||
// Relay URL - fixed port for MCP bridge
|
// Relay URL - fixed port for MCP bridge
|
||||||
const RELAY_URL = 'ws://localhost:9988/extension';
|
const RELAY_URL = 'ws://localhost:9988/extension';
|
||||||
|
|
||||||
type ConnectionState = 'disconnected' | 'reconnecting' | 'connected';
|
type ConnectionState = 'disconnected' | 'reconnecting' | 'connected' | 'error';
|
||||||
|
|
||||||
interface ExtensionState {
|
interface ExtensionState {
|
||||||
connection: RelayConnection | undefined;
|
connection: RelayConnection | undefined;
|
||||||
connectedTabs: Map<number, string>;
|
connectedTabs: Map<number, string>;
|
||||||
connectionState: ConnectionState;
|
connectionState: ConnectionState;
|
||||||
currentTabId: number | undefined;
|
currentTabId: number | undefined;
|
||||||
|
errorText: string | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const useExtensionStore = create<ExtensionState>(() => ({
|
const useExtensionStore = create<ExtensionState>(() => ({
|
||||||
@@ -19,110 +20,81 @@ const useExtensionStore = create<ExtensionState>(() => ({
|
|||||||
connectedTabs: new Map(),
|
connectedTabs: new Map(),
|
||||||
connectionState: 'disconnected',
|
connectionState: 'disconnected',
|
||||||
currentTabId: undefined,
|
currentTabId: undefined,
|
||||||
|
errorText: undefined,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
async function updateIcon(tabId: number, state: 'connected' | 'disconnected' | 'connecting'): Promise<void> {
|
const icons = {
|
||||||
try {
|
connected: {
|
||||||
switch (state) {
|
path: {
|
||||||
case 'connected':
|
'16': '/icons/icon-green-16.png',
|
||||||
await chrome.action.setIcon({
|
'32': '/icons/icon-green-32.png',
|
||||||
tabId,
|
'48': '/icons/icon-green-48.png',
|
||||||
path: {
|
'128': '/icons/icon-green-128.png'
|
||||||
'16': '/icons/icon-green-16.png',
|
},
|
||||||
'32': '/icons/icon-green-32.png',
|
title: 'Connected - Click to disconnect',
|
||||||
'48': '/icons/icon-green-48.png',
|
badgeText: '',
|
||||||
'128': '/icons/icon-green-128.png'
|
badgeColor: undefined
|
||||||
}
|
},
|
||||||
});
|
connecting: {
|
||||||
await chrome.action.setBadgeText({ tabId, text: '' });
|
path: {
|
||||||
await chrome.action.setTitle({ tabId, title: 'Connected - Click to disconnect' });
|
'16': '/icons/icon-gray-16.png',
|
||||||
break;
|
'32': '/icons/icon-gray-32.png',
|
||||||
|
'48': '/icons/icon-gray-48.png',
|
||||||
case 'connecting':
|
'128': '/icons/icon-gray-128.png'
|
||||||
await chrome.action.setIcon({
|
},
|
||||||
tabId,
|
title: 'Connecting...',
|
||||||
path: {
|
badgeText: '...',
|
||||||
'16': '/icons/icon-gray-16.png',
|
badgeColor: '#FF9800'
|
||||||
'32': '/icons/icon-gray-32.png',
|
},
|
||||||
'48': '/icons/icon-gray-48.png',
|
disconnected: {
|
||||||
'128': '/icons/icon-gray-128.png'
|
path: {
|
||||||
}
|
'16': '/icons/icon-gray-16.png',
|
||||||
});
|
'32': '/icons/icon-gray-32.png',
|
||||||
await chrome.action.setBadgeText({ tabId, text: '...' });
|
'48': '/icons/icon-gray-48.png',
|
||||||
await chrome.action.setBadgeBackgroundColor({ tabId, color: '#FF9800' });
|
'128': '/icons/icon-gray-48.png'
|
||||||
await chrome.action.setTitle({ tabId, title: 'Connecting...' });
|
},
|
||||||
break;
|
title: 'Click to attach debugger',
|
||||||
|
badgeText: '',
|
||||||
case 'disconnected':
|
badgeColor: undefined
|
||||||
default:
|
},
|
||||||
await chrome.action.setIcon({
|
error: {
|
||||||
tabId,
|
path: {
|
||||||
path: {
|
'16': '/icons/icon-gray-16.png',
|
||||||
'16': '/icons/icon-gray-16.png',
|
'32': '/icons/icon-gray-32.png',
|
||||||
'32': '/icons/icon-gray-32.png',
|
'48': '/icons/icon-gray-48.png',
|
||||||
'48': '/icons/icon-gray-48.png',
|
'128': '/icons/icon-gray-48.png'
|
||||||
'128': '/icons/icon-gray-128.png'
|
},
|
||||||
}
|
title: 'Error',
|
||||||
});
|
badgeText: '!',
|
||||||
await chrome.action.setBadgeText({ tabId, text: '' });
|
badgeColor: '#f44336'
|
||||||
await chrome.action.setTitle({ tabId, title: 'Click to attach debugger' });
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
debugLog(`Error updating icon: ${error.message}`);
|
|
||||||
}
|
}
|
||||||
}
|
} as const;
|
||||||
|
|
||||||
useExtensionStore.subscribe((state, prevState) => {
|
useExtensionStore.subscribe(async (state, prevState) => {
|
||||||
const prevTabs = new Set(prevState.connectedTabs.keys());
|
console.log(state)
|
||||||
const currentTabs = new Set(state.connectedTabs.keys());
|
const { connectionState, connectedTabs, errorText } = state;
|
||||||
const prevConnectionState = prevState.connectionState;
|
|
||||||
const currentConnectionState = state.connectionState;
|
|
||||||
const prevCurrentTabId = prevState.currentTabId;
|
|
||||||
const currentTabId = state.currentTabId;
|
|
||||||
|
|
||||||
const connectionStateChanged = prevConnectionState !== currentConnectionState;
|
const tabs = await chrome.tabs.query({});
|
||||||
const currentTabChanged = prevCurrentTabId !== currentTabId;
|
const allTabIds = [undefined, ...tabs.map(tab => tab.id).filter((id): id is number => id !== undefined)];
|
||||||
|
|
||||||
const tabsToUpdate = new Set<number>();
|
for (const tabId of allTabIds) {
|
||||||
|
const iconConfig = (() => {
|
||||||
if (connectionStateChanged) {
|
if (connectionState === 'error') {
|
||||||
debugLog('Connection state changed:', prevConnectionState, '->', currentConnectionState);
|
return icons.error;
|
||||||
for (const tabId of currentTabs) {
|
}
|
||||||
tabsToUpdate.add(tabId);
|
if (connectionState === 'reconnecting') {
|
||||||
}
|
return icons.connecting;
|
||||||
}
|
}
|
||||||
|
if (tabId !== undefined && connectedTabs.has(tabId) && connectionState === 'connected') {
|
||||||
const addedTabs = [...currentTabs].filter(id => !prevTabs.has(id));
|
return icons.connected;
|
||||||
const removedTabs = [...prevTabs].filter(id => !currentTabs.has(id));
|
}
|
||||||
|
return icons.disconnected;
|
||||||
for (const tabId of addedTabs) {
|
})();
|
||||||
tabsToUpdate.add(tabId);
|
const title = connectionState === 'error' && errorText ? errorText : iconConfig.title;
|
||||||
}
|
void chrome.action.setIcon({ tabId, path: iconConfig.path });
|
||||||
|
void chrome.action.setTitle({ tabId, title });
|
||||||
for (const tabId of removedTabs) {
|
if (iconConfig.badgeColor) void chrome.action.setBadgeBackgroundColor({ tabId, color: iconConfig.badgeColor });
|
||||||
tabsToUpdate.add(tabId);
|
void chrome.action.setBadgeText({ tabId, text: iconConfig.badgeText });
|
||||||
}
|
|
||||||
|
|
||||||
if (currentTabChanged && currentTabId !== undefined) {
|
|
||||||
tabsToUpdate.add(currentTabId);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const tabId of tabsToUpdate) {
|
|
||||||
const isTracked = currentTabs.has(tabId);
|
|
||||||
let iconState: 'connected' | 'disconnected' | 'connecting';
|
|
||||||
|
|
||||||
if (currentConnectionState === 'disconnected') {
|
|
||||||
iconState = 'disconnected';
|
|
||||||
} else if (!isTracked) {
|
|
||||||
iconState = 'disconnected';
|
|
||||||
} else if (currentConnectionState === 'connected') {
|
|
||||||
iconState = 'connected';
|
|
||||||
} else {
|
|
||||||
iconState = 'connecting';
|
|
||||||
}
|
|
||||||
|
|
||||||
void updateIcon(tabId, iconState);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -133,11 +105,12 @@ async function ensureConnection(): Promise<void> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
useExtensionStore.setState({ connectionState: 'reconnecting' });
|
|
||||||
debugLog('No existing connection, creating new relay connection');
|
debugLog('No existing connection, creating new relay connection');
|
||||||
debugLog('Waiting for server at http://localhost:9988...');
|
debugLog('Waiting for server at http://localhost:9988...');
|
||||||
|
|
||||||
while (useExtensionStore.getState().connectionState !== 'disconnected') {
|
useExtensionStore.setState({ connectionState: 'reconnecting' });
|
||||||
|
while (true) {
|
||||||
try {
|
try {
|
||||||
await fetch('http://localhost:9988', { method: 'HEAD' });
|
await fetch('http://localhost:9988', { method: 'HEAD' });
|
||||||
debugLog('Server is available');
|
debugLog('Server is available');
|
||||||
@@ -148,11 +121,6 @@ async function ensureConnection(): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (useExtensionStore.getState().connectionState === 'disconnected') {
|
|
||||||
debugLog('Connection cancelled by user');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
debugLog('Server is ready, creating WebSocket connection to:', RELAY_URL);
|
debugLog('Server is ready, creating WebSocket connection to:', RELAY_URL);
|
||||||
const socket = new WebSocket(RELAY_URL);
|
const socket = new WebSocket(RELAY_URL);
|
||||||
debugLog('WebSocket created, initial readyState:', socket.readyState, '(0=CONNECTING, 1=OPEN, 2=CLOSING, 3=CLOSED)');
|
debugLog('WebSocket created, initial readyState:', socket.readyState, '(0=CONNECTING, 1=OPEN, 2=CLOSING, 3=CLOSED)');
|
||||||
@@ -220,15 +188,19 @@ async function ensureConnection(): Promise<void> {
|
|||||||
debugLog('No tabs to reconnect');
|
debugLog('No tabs to reconnect');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onTabDetached: (tabId) => {
|
onTabDetached: (tabId, reason) => {
|
||||||
debugLog('=== Manual tab detachment detected for tab:', tabId, '===');
|
debugLog('=== Manual tab detachment detected for tab:', tabId, '===');
|
||||||
debugLog('User closed debugger via Chrome automation bar');
|
debugLog('User closed debugger via Chrome automation bar');
|
||||||
|
|
||||||
useExtensionStore.setState((state) => {
|
useExtensionStore.setState((state) => {
|
||||||
const newTabs = new Map(state.connectedTabs);
|
const newTabs = new Map(state.connectedTabs);
|
||||||
newTabs.delete(tabId);
|
newTabs.delete(tabId);
|
||||||
return { connectionState: 'disconnected', connectedTabs: newTabs };
|
return { connectedTabs: newTabs };
|
||||||
});
|
});
|
||||||
|
if (reason=== chrome.debugger.DetachReason.CANCELED_BY_USER) {
|
||||||
|
// if user cancels debugger. disconnect everything
|
||||||
|
useExtensionStore.setState({ connectionState: 'disconnected' });
|
||||||
|
}
|
||||||
debugLog('Removed tab from _connectedTabs map');
|
debugLog('Removed tab from _connectedTabs map');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -270,20 +242,8 @@ async function connectTab(tabId: number): Promise<void> {
|
|||||||
useExtensionStore.setState((state) => {
|
useExtensionStore.setState((state) => {
|
||||||
const newTabs = new Map(state.connectedTabs);
|
const newTabs = new Map(state.connectedTabs);
|
||||||
newTabs.delete(tabId);
|
newTabs.delete(tabId);
|
||||||
return { connectedTabs: newTabs };
|
return { connectedTabs: newTabs, connectionState: 'error', errorText: `Error: ${error.message}` };
|
||||||
});
|
});
|
||||||
|
|
||||||
chrome.action.setBadgeText({ tabId, text: '!' });
|
|
||||||
chrome.action.setBadgeBackgroundColor({ tabId, color: '#f44336' });
|
|
||||||
chrome.action.setTitle({ tabId, title: `Error: ${error.message}` });
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
const { connectedTabs } = useExtensionStore.getState();
|
|
||||||
if (!connectedTabs.has(tabId)) {
|
|
||||||
chrome.action.setBadgeText({ tabId, text: '' });
|
|
||||||
chrome.action.setTitle({ tabId, title: 'Click to attach debugger' });
|
|
||||||
}
|
|
||||||
}, 3000);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -370,14 +330,11 @@ async function reconnect(): Promise<void> {
|
|||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
debugLog('=== Reconnection failed ===', error);
|
debugLog('=== Reconnection failed ===', error);
|
||||||
|
|
||||||
const { connectedTabs: failedTabs } = useExtensionStore.getState();
|
useExtensionStore.setState({
|
||||||
for (const tabId of failedTabs.keys()) {
|
connectedTabs: new Map(),
|
||||||
chrome.action.setBadgeText({ tabId, text: '!' });
|
connectionState: 'error',
|
||||||
chrome.action.setBadgeBackgroundColor({ tabId, color: '#f44336' });
|
errorText: 'Reconnection failed - Click to retry'
|
||||||
chrome.action.setTitle({ tabId, title: 'Reconnection failed - Click to retry' });
|
});
|
||||||
}
|
|
||||||
|
|
||||||
useExtensionStore.setState({ connectedTabs: new Map(), connectionState: 'disconnected' });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -403,8 +360,8 @@ async function onActionClicked(tab: chrome.tabs.Tab): Promise<void> {
|
|||||||
|
|
||||||
const { connectedTabs, connectionState, connection } = useExtensionStore.getState();
|
const { connectedTabs, connectionState, connection } = useExtensionStore.getState();
|
||||||
|
|
||||||
if (connectionState === 'reconnecting') {
|
if (connectionState === 'reconnecting' || connectionState === 'error') {
|
||||||
debugLog('User clicked during reconnection, canceling reconnection and disconnecting all tabs');
|
debugLog('User clicked during reconnection/error, canceling and disconnecting all tabs');
|
||||||
|
|
||||||
const tabsToDisconnect = Array.from(connectedTabs.keys());
|
const tabsToDisconnect = Array.from(connectedTabs.keys());
|
||||||
|
|
||||||
@@ -412,7 +369,7 @@ async function onActionClicked(tab: chrome.tabs.Tab): Promise<void> {
|
|||||||
connection?.detachTab(tabId);
|
connection?.detachTab(tabId);
|
||||||
}
|
}
|
||||||
|
|
||||||
useExtensionStore.setState({ connectionState: 'disconnected', connectedTabs: new Map() });
|
useExtensionStore.setState({ connectionState: 'disconnected', connectedTabs: new Map(), errorText: undefined });
|
||||||
|
|
||||||
if (connection) {
|
if (connection) {
|
||||||
connection.close('User cancelled reconnection');
|
connection.close('User cancelled reconnection');
|
||||||
|
|||||||
@@ -28,10 +28,10 @@ Return value:
|
|||||||
- generic [ref=e21]:
|
- generic [ref=e21]:
|
||||||
- generic: ⌘
|
- generic: ⌘
|
||||||
- generic: K
|
- generic: K
|
||||||
- link "98.7k" [ref=e22] [cursor=pointer]:
|
- link "99.8k" [ref=e22] [cursor=pointer]:
|
||||||
- /url: https://github.com/shadcn-ui/ui
|
- /url: https://github.com/shadcn-ui/ui
|
||||||
- img
|
- img
|
||||||
- generic [ref=e23]: 98.7k
|
- generic [ref=e23]: 99.8k
|
||||||
- button "Toggle layout" [ref=e24]:
|
- button "Toggle layout" [ref=e24]:
|
||||||
- generic [ref=e25]: Toggle layout
|
- generic [ref=e25]: Toggle layout
|
||||||
- img
|
- img
|
||||||
|
|||||||
Reference in New Issue
Block a user