fix: remove ensureResponseListener; remove isPageAgentMessage

This commit is contained in:
Simon
2026-01-26 19:17:52 +08:00
parent f18b756390
commit 3bf616dde1
4 changed files with 46 additions and 122 deletions

View File

@@ -15,7 +15,6 @@ import {
type ExtensionMessage, type ExtensionMessage,
type QueryResponseMessage, type QueryResponseMessage,
type RPCCallMessage, type RPCCallMessage,
type RPCResponseMessage,
type TabEventMessage, type TabEventMessage,
generateMessageId, generateMessageId,
isExtensionMessage, isExtensionMessage,
@@ -42,9 +41,9 @@ chrome.runtime.onMessage.addListener(
switch (msg.type) { switch (msg.type) {
case 'rpc:call': case 'rpc:call':
// SidePanel → SW: Forward RPC to content script // SidePanel → SW: Forward RPC to content script, return result via sendResponse
handleRPCCall(msg as RPCCallMessage) handleRPCCall(msg as RPCCallMessage, sendResponse)
return false // No sync response needed return true // Async response
case 'cs:query': case 'cs:query':
// ContentScript → SW: Forward query to sidepanel // ContentScript → SW: Forward query to sidepanel
@@ -59,15 +58,18 @@ chrome.runtime.onMessage.addListener(
/** /**
* Forward RPC call from SidePanel to ContentScript * Forward RPC call from SidePanel to ContentScript
* Uses sendResponse to return result directly (MV3 compliant)
*/ */
async function handleRPCCall(msg: RPCCallMessage): Promise<void> { async function handleRPCCall(
const { id, tabId, method, args } = msg msg: RPCCallMessage,
sendResponse: (response: { success: boolean; result?: unknown; error?: string }) => void
): Promise<void> {
const { tabId, method, args } = msg
// Create message for content script // Create message for content script
const csMessage: CSRPCMessage = { const csMessage: CSRPCMessage = {
isPageAgentMessage: true,
type: 'cs:rpc', type: 'cs:rpc',
id, id: msg.id,
method, method,
args, args,
} }
@@ -75,27 +77,11 @@ async function handleRPCCall(msg: RPCCallMessage): Promise<void> {
try { try {
// Send to content script and wait for response // Send to content script and wait for response
const result = await chrome.tabs.sendMessage(tabId, csMessage) const result = await chrome.tabs.sendMessage(tabId, csMessage)
sendResponse({ success: true, result })
// Forward response back to sidepanel
const response: RPCResponseMessage = {
isPageAgentMessage: true,
type: 'rpc:response',
id,
success: true,
result,
}
await chrome.runtime.sendMessage(response)
} catch (error) { } catch (error) {
// Forward error back to sidepanel sendResponse({
const response: RPCResponseMessage = {
isPageAgentMessage: true,
type: 'rpc:response',
id,
success: false, success: false,
error: error instanceof Error ? error.message : String(error), error: error instanceof Error ? error.message : String(error),
}
await chrome.runtime.sendMessage(response).catch(() => {
// Sidepanel may be closed
}) })
} }
} }
@@ -120,7 +106,6 @@ async function handleCSQuery(
// Forward response back to content script // Forward response back to content script
if (sender.tab?.id) { if (sender.tab?.id) {
const queryResponse: QueryResponseMessage = { const queryResponse: QueryResponseMessage = {
isPageAgentMessage: true,
type: 'query:response', type: 'query:response',
id, id,
result: response, result: response,
@@ -131,7 +116,6 @@ async function handleCSQuery(
// Sidepanel not open or no response, return default // Sidepanel not open or no response, return default
if (sender.tab?.id) { if (sender.tab?.id) {
const queryResponse: QueryResponseMessage = { const queryResponse: QueryResponseMessage = {
isPageAgentMessage: true,
type: 'query:response', type: 'query:response',
id, id,
result: queryType === 'shouldShowMask' ? false : null, result: queryType === 'shouldShowMask' ? false : null,
@@ -150,7 +134,6 @@ async function handleCSQuery(
*/ */
chrome.tabs.onRemoved.addListener((tabId) => { chrome.tabs.onRemoved.addListener((tabId) => {
const message: TabEventMessage = { const message: TabEventMessage = {
isPageAgentMessage: true,
type: 'tab:event', type: 'tab:event',
id: generateMessageId(), id: generateMessageId(),
eventType: 'removed', eventType: 'removed',
@@ -169,7 +152,6 @@ chrome.tabs.onUpdated.addListener((tabId, changeInfo) => {
if (!changeInfo.status) return if (!changeInfo.status) return
const message: TabEventMessage = { const message: TabEventMessage = {
isPageAgentMessage: true,
type: 'tab:event', type: 'tab:event',
id: generateMessageId(), id: generateMessageId(),
eventType: 'updated', eventType: 'updated',
@@ -189,7 +171,6 @@ chrome.tabs.onUpdated.addListener((tabId, changeInfo) => {
*/ */
chrome.tabs.onActivated.addListener((activeInfo) => { chrome.tabs.onActivated.addListener((activeInfo) => {
const message: TabEventMessage = { const message: TabEventMessage = {
isPageAgentMessage: true,
type: 'tab:event', type: 'tab:event',
id: generateMessageId(), id: generateMessageId(),
eventType: 'activated', eventType: 'activated',
@@ -210,7 +191,6 @@ chrome.windows.onFocusChanged.addListener((windowId) => {
// windowId is chrome.windows.WINDOW_ID_NONE (-1) when all windows lose focus // windowId is chrome.windows.WINDOW_ID_NONE (-1) when all windows lose focus
const focused = windowId !== chrome.windows.WINDOW_ID_NONE const focused = windowId !== chrome.windows.WINDOW_ID_NONE
const message: TabEventMessage = { const message: TabEventMessage = {
isPageAgentMessage: true,
type: 'tab:event', type: 'tab:event',
id: generateMessageId(), id: generateMessageId(),
eventType: 'windowFocusChanged', eventType: 'windowFocusChanged',

View File

@@ -81,7 +81,6 @@ async function queryShouldShowMask(getController: () => PageController): Promise
const queryId = generateMessageId() const queryId = generateMessageId()
const queryMessage: CSQueryMessage = { const queryMessage: CSQueryMessage = {
isPageAgentMessage: true,
type: 'cs:query', type: 'cs:query',
id: queryId, id: queryId,
queryType: 'shouldShowMask', queryType: 'shouldShowMask',

View File

@@ -1,13 +1,13 @@
/** /**
* Message Protocol for PageAgentExt * Message Protocol for PageAgentExt
* *
* NEW ARCHITECTURE (MV3 compliant): * MV3 Compliant Architecture:
* - SidePanel hosts the agent, all state lives there * - SidePanel hosts the agent, all state lives there
* - Background (SW) is a stateless message relay * - Background (SW) is a stateless message relay
* - Content Script runs PageController * - Content Script runs PageController
* *
* Message flows: * Message flows:
* 1. RPC: SidePanel → SW → ContentScript → SW → SidePanel (PageController calls) * 1. RPC: SidePanel → SW → ContentScript → sendResponse (PageController calls)
* 2. Query: ContentScript → SW → SidePanel → SW → ContentScript (mask state check) * 2. Query: ContentScript → SW → SidePanel → SW → ContentScript (mask state check)
* 3. Events: SW → SidePanel (tab events from chrome.tabs API) * 3. Events: SW → SidePanel (tab events from chrome.tabs API)
*/ */
@@ -52,8 +52,7 @@ export interface ScrollHorizontallyOptions {
/** Message type identifier */ /** Message type identifier */
type MessageType = type MessageType =
| 'rpc:call' // SidePanel → SW: RPC call to content script | 'rpc:call' // SidePanel → SW: RPC call to content script (response via sendResponse)
| 'rpc:response' // SW → SidePanel: RPC response from content script
| 'cs:rpc' // SW → ContentScript: Forwarded RPC call | 'cs:rpc' // SW → ContentScript: Forwarded RPC call
| 'cs:query' // ContentScript → SW: Query to sidepanel | 'cs:query' // ContentScript → SW: Query to sidepanel
| 'query:response' // SW → ContentScript: Query response | 'query:response' // SW → ContentScript: Query response
@@ -61,7 +60,6 @@ type MessageType =
/** Base message structure */ /** Base message structure */
interface BaseMessage { interface BaseMessage {
isPageAgentMessage: true
type: MessageType type: MessageType
id: string // Unique message ID for request-response matching id: string // Unique message ID for request-response matching
} }
@@ -78,14 +76,6 @@ export interface RPCCallMessage extends BaseMessage {
args: unknown[] args: unknown[]
} }
/** SW → SidePanel: Response from PageController */
export interface RPCResponseMessage extends BaseMessage {
type: 'rpc:response'
success: boolean
result?: unknown
error?: string
}
/** SW → ContentScript: Forwarded RPC call */ /** SW → ContentScript: Forwarded RPC call */
export interface CSRPCMessage extends BaseMessage { export interface CSRPCMessage extends BaseMessage {
type: 'cs:rpc' type: 'cs:rpc'
@@ -143,7 +133,6 @@ export interface TabEventMessage extends BaseMessage {
/** All message types */ /** All message types */
export type ExtensionMessage = export type ExtensionMessage =
| RPCCallMessage | RPCCallMessage
| RPCResponseMessage
| CSRPCMessage | CSRPCMessage
| CSQueryMessage | CSQueryMessage
| QueryResponseMessage | QueryResponseMessage
@@ -158,12 +147,16 @@ export function generateMessageId(): string {
return `${Date.now()}-${Math.random().toString(36).slice(2, 8)}` return `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
} }
/** Type guard for our messages */ /** Known message types for type guard */
const MESSAGE_TYPES = new Set<string>([
'rpc:call',
'cs:rpc',
'cs:query',
'query:response',
'tab:event',
])
/** Type guard - checks if message has a known type */
export function isExtensionMessage(msg: unknown): msg is ExtensionMessage { export function isExtensionMessage(msg: unknown): msg is ExtensionMessage {
return ( return typeof msg === 'object' && msg !== null && MESSAGE_TYPES.has((msg as any).type)
typeof msg === 'object' &&
msg !== null &&
'isPageAgentMessage' in msg &&
(msg as any).isPageAgentMessage === true
)
} }

View File

@@ -4,17 +4,18 @@
* This module provides RPC functionality from SidePanel to ContentScript * This module provides RPC functionality from SidePanel to ContentScript
* via the Background (SW) relay. * via the Background (SW) relay.
* *
* Flow: SidePanel → SW (relay) → ContentScript → SW → SidePanel * Flow: SidePanel → SW (relay) → ContentScript → sendResponse → SidePanel
*
* MV3 Compliant: Uses chrome.runtime.sendMessage with direct sendResponse,
* no pending calls map or custom response listeners needed.
*/ */
import { import {
type ActionResult, type ActionResult,
type BrowserState, type BrowserState,
type RPCCallMessage, type RPCCallMessage,
type RPCResponseMessage,
type ScrollHorizontallyOptions, type ScrollHorizontallyOptions,
type ScrollOptions, type ScrollOptions,
generateMessageId, generateMessageId,
isExtensionMessage,
} from './protocol' } from './protocol'
/** RPC configuration */ /** RPC configuration */
@@ -23,52 +24,6 @@ const RPC_CONFIG = {
maxRetries: 3, maxRetries: 3,
/** Base delay between retries in ms (exponential backoff) */ /** Base delay between retries in ms (exponential backoff) */
retryDelayMs: 500, retryDelayMs: 500,
/** Timeout for individual RPC call in ms */
callTimeoutMs: 30000,
}
/** Pending RPC calls waiting for response */
const pendingCalls = new Map<
string,
{
resolve: (value: unknown) => void
reject: (error: Error) => void
timeout: ReturnType<typeof setTimeout>
}
>()
/** Whether the response listener is registered */
let listenerRegistered = false
/**
* Register the RPC response listener (called once)
*/
function ensureResponseListener(): void {
if (listenerRegistered) return
listenerRegistered = true
chrome.runtime.onMessage.addListener((message: unknown) => {
if (!isExtensionMessage(message)) return
if (message.type !== 'rpc:response') return
const response = message as RPCResponseMessage
const pending = pendingCalls.get(response.id)
if (!pending) {
console.debug('[RPC] Received response for unknown call:', response.id)
return
}
pendingCalls.delete(response.id)
clearTimeout(pending.timeout)
if (response.success) {
pending.resolve(response.result)
} else {
pending.reject(new Error(response.error || 'RPC call failed'))
}
})
console.debug('[RPC] Response listener registered')
} }
/** /**
@@ -96,43 +51,40 @@ async function tabExists(tabId: number): Promise<boolean> {
export class RPCError extends Error { export class RPCError extends Error {
constructor( constructor(
message: string, message: string,
public readonly code: 'TAB_CLOSED' | 'CONTENT_SCRIPT_NOT_READY' | 'RPC_FAILED' | 'TIMEOUT' public readonly code: 'TAB_CLOSED' | 'CONTENT_SCRIPT_NOT_READY' | 'RPC_FAILED'
) { ) {
super(message) super(message)
this.name = 'RPCError' this.name = 'RPCError'
} }
} }
/** Response type from background script */
interface RPCResponse {
success: boolean
result?: unknown
error?: string
}
/** /**
* Make a single RPC call (no retry) * Make a single RPC call (no retry)
* Uses chrome.runtime.sendMessage which returns the response directly via sendResponse
*/ */
async function callOnce(tabId: number, method: string, args: unknown[]): Promise<unknown> { async function callOnce(tabId: number, method: string, args: unknown[]): Promise<unknown> {
ensureResponseListener()
const id = generateMessageId()
const message: RPCCallMessage = { const message: RPCCallMessage = {
isPageAgentMessage: true,
type: 'rpc:call', type: 'rpc:call',
id, id: generateMessageId(),
tabId, tabId,
method, method,
args, args,
} }
return new Promise((resolve, reject) => { const response = (await chrome.runtime.sendMessage(message)) as RPCResponse
const timeout = setTimeout(() => {
pendingCalls.delete(id)
reject(new RPCError(`RPC ${method} timed out`, 'TIMEOUT'))
}, RPC_CONFIG.callTimeoutMs)
pendingCalls.set(id, { resolve, reject, timeout }) if (response?.success) {
return response.result
chrome.runtime.sendMessage(message).catch((error: Error) => { } else {
pendingCalls.delete(id) throw new Error(response?.error || 'RPC call failed')
clearTimeout(timeout) }
reject(error)
})
})
} }
/** /**