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

View File

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

View File

@@ -1,13 +1,13 @@
/**
* Message Protocol for PageAgentExt
*
* NEW ARCHITECTURE (MV3 compliant):
* MV3 Compliant Architecture:
* - SidePanel hosts the agent, all state lives there
* - Background (SW) is a stateless message relay
* - Content Script runs PageController
*
* 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)
* 3. Events: SW → SidePanel (tab events from chrome.tabs API)
*/
@@ -52,8 +52,7 @@ export interface ScrollHorizontallyOptions {
/** Message type identifier */
type MessageType =
| 'rpc:call' // SidePanel → SW: RPC call to content script
| 'rpc:response' // SW → SidePanel: RPC response from content script
| 'rpc:call' // SidePanel → SW: RPC call to content script (response via sendResponse)
| 'cs:rpc' // SW → ContentScript: Forwarded RPC call
| 'cs:query' // ContentScript → SW: Query to sidepanel
| 'query:response' // SW → ContentScript: Query response
@@ -61,7 +60,6 @@ type MessageType =
/** Base message structure */
interface BaseMessage {
isPageAgentMessage: true
type: MessageType
id: string // Unique message ID for request-response matching
}
@@ -78,14 +76,6 @@ export interface RPCCallMessage extends BaseMessage {
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 */
export interface CSRPCMessage extends BaseMessage {
type: 'cs:rpc'
@@ -143,7 +133,6 @@ export interface TabEventMessage extends BaseMessage {
/** All message types */
export type ExtensionMessage =
| RPCCallMessage
| RPCResponseMessage
| CSRPCMessage
| CSQueryMessage
| QueryResponseMessage
@@ -158,12 +147,16 @@ export function generateMessageId(): string {
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 {
return (
typeof msg === 'object' &&
msg !== null &&
'isPageAgentMessage' in msg &&
(msg as any).isPageAgentMessage === true
)
return typeof msg === 'object' && msg !== null && MESSAGE_TYPES.has((msg as any).type)
}

View File

@@ -4,17 +4,18 @@
* This module provides RPC functionality from SidePanel to ContentScript
* 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 {
type ActionResult,
type BrowserState,
type RPCCallMessage,
type RPCResponseMessage,
type ScrollHorizontallyOptions,
type ScrollOptions,
generateMessageId,
isExtensionMessage,
} from './protocol'
/** RPC configuration */
@@ -23,52 +24,6 @@ const RPC_CONFIG = {
maxRetries: 3,
/** Base delay between retries in ms (exponential backoff) */
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 {
constructor(
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)
this.name = 'RPCError'
}
}
/** Response type from background script */
interface RPCResponse {
success: boolean
result?: unknown
error?: string
}
/**
* 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> {
ensureResponseListener()
const id = generateMessageId()
const message: RPCCallMessage = {
isPageAgentMessage: true,
type: 'rpc:call',
id,
id: generateMessageId(),
tabId,
method,
args,
}
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
pendingCalls.delete(id)
reject(new RPCError(`RPC ${method} timed out`, 'TIMEOUT'))
}, RPC_CONFIG.callTimeoutMs)
const response = (await chrome.runtime.sendMessage(message)) as RPCResponse
pendingCalls.set(id, { resolve, reject, timeout })
chrome.runtime.sendMessage(message).catch((error: Error) => {
pendingCalls.delete(id)
clearTimeout(timeout)
reject(error)
})
})
if (response?.success) {
return response.result
} else {
throw new Error(response?.error || 'RPC call failed')
}
}
/**