fix: remove ensureResponseListener; remove isPageAgentMessage
This commit is contained in:
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user