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