From de00458c06223012c7745a3f023aac25b0b7c597 Mon Sep 17 00:00:00 2001 From: Camille Lawrence Date: Tue, 27 Jan 2026 18:30:38 -0500 Subject: [PATCH 1/6] feat: add iframe support for Apollo Client detection --- development/client/public/iframe.html | 50 +++++++++ development/client/src/App.tsx | 1 + src/extension/background/background.ts | 112 +++++++++++++------- src/extension/chrome/manifest.json | 5 +- src/extension/firefox/manifest.json | 5 +- src/extension/rpc.ts | 9 ++ src/extension/tab/handleExplorerRequests.ts | 4 +- src/extension/tab/hook.ts | 48 +++++++-- 8 files changed, 181 insertions(+), 53 deletions(-) create mode 100644 development/client/public/iframe.html diff --git a/development/client/public/iframe.html b/development/client/public/iframe.html new file mode 100644 index 000000000..f1df2fb4a --- /dev/null +++ b/development/client/public/iframe.html @@ -0,0 +1,50 @@ + + + + + + + Color App in an IFrame + + + + +

🔍 Iframe Apollo Client Test Page

+

+ This page embeds the Color App in an iframe. With all_frames: true in the manifest, + Apollo Client DevTools should detect the Apollo Client instance running inside this iframe. +

+
+ +
+ + + diff --git a/development/client/src/App.tsx b/development/client/src/App.tsx index a316c351b..add4ef32b 100644 --- a/development/client/src/App.tsx +++ b/development/client/src/App.tsx @@ -56,6 +56,7 @@ function App() { Favorites Lookup Playground + Iframe Test
"], "js": ["tab.js"], "run_at": "document_start", "all_frames": true }, + { + "matches": [""], + "js": ["tab.js"], + "run_at": "document_start", + "all_frames": true + }, { "matches": [""], "js": ["hook.js"], diff --git a/src/extension/firefox/manifest.json b/src/extension/firefox/manifest.json index 7fe2b0eb2..336319c98 100644 --- a/src/extension/firefox/manifest.json +++ b/src/extension/firefox/manifest.json @@ -18,7 +18,12 @@ } ], "content_scripts": [ - { "matches": [""], "js": ["tab.js"], "run_at": "document_start", "all_frames": true }, + { + "matches": [""], + "js": ["tab.js"], + "run_at": "document_start", + "all_frames": true + }, { "matches": [""], "js": ["hook.js"], diff --git a/src/extension/tab/hook.ts b/src/extension/tab/hook.ts index a1c76a638..49e2b5b79 100755 --- a/src/extension/tab/hook.ts +++ b/src/extension/tab/hook.ts @@ -87,41 +87,29 @@ handleRpc("getClient", (clientId) => { return getClientInfo(handler.getClient()); }); -handleRpc( - "getV3Queries", - (clientId) => { - const handler = getHandlerByClientId(clientId); - if (!handler) return SKIP_RESPONSE as any; - return handler.getQueries(); - } -); - -handleRpc( - "getV4Queries", - (clientId) => { - const handler = getHandlerByClientId(clientId); - if (!handler) return SKIP_RESPONSE as any; - return handler.getQueries(); - } -); - -handleRpc( - "getV3Mutations", - (clientId) => { - const handler = getHandlerByClientId(clientId); - if (!handler) return SKIP_RESPONSE as any; - return handler.getMutations(); - } -); - -handleRpc( - "getV4Mutations", - (clientId) => { - const handler = getHandlerByClientId(clientId); - if (!handler) return SKIP_RESPONSE as any; - return handler.getMutations(); - } -); +handleRpc("getV3Queries", (clientId) => { + const handler = getHandlerByClientId(clientId); + if (!handler) return SKIP_RESPONSE as any; + return handler.getQueries(); +}); + +handleRpc("getV4Queries", (clientId) => { + const handler = getHandlerByClientId(clientId); + if (!handler) return SKIP_RESPONSE as any; + return handler.getQueries(); +}); + +handleRpc("getV3Mutations", (clientId) => { + const handler = getHandlerByClientId(clientId); + if (!handler) return SKIP_RESPONSE as any; + return handler.getMutations(); +}); + +handleRpc("getV4Mutations", (clientId) => { + const handler = getHandlerByClientId(clientId); + if (!handler) return SKIP_RESPONSE as any; + return handler.getMutations(); +}); handleRpc("getCache", (clientId) => { const client = getClientById(clientId as any); From 37fc14a55f0474ba1ad73787c55934d76b612f7b Mon Sep 17 00:00:00 2001 From: Camille Lawrence Date: Wed, 28 Jan 2026 16:34:39 -0500 Subject: [PATCH 3/6] feat(background): route client-specific RPC messages by frameId --- src/extension/background/background.ts | 94 ++++++++++++++++++++++---- 1 file changed, 82 insertions(+), 12 deletions(-) diff --git a/src/extension/background/background.ts b/src/extension/background/background.ts index f7a1bcad6..22f69b3be 100644 --- a/src/extension/background/background.ts +++ b/src/extension/background/background.ts @@ -3,12 +3,33 @@ // https://github.com/facebook/react/blob/18a9dd1c60fdb711982f32ce5d91acfe8f158fe1/LICENSE import browser from "webextension-polyfill"; import "./errorcodes"; -import { createDevtoolsMessage, MessageType } from "../messages"; +import { + createDevtoolsMessage, + isDevtoolsMessage, + MessageType, +} from "../messages"; + +// Type guards for message routing +function isActorMessage( + message: unknown +): message is { + type: MessageType.Actor; + message: { type: string; payload?: { id?: string }; clientId?: string }; +} { + return isDevtoolsMessage(message) && message.type === MessageType.Actor; +} + +function isRPCRequestMessage( + message: unknown +): message is { type: MessageType.RPCRequest; params: unknown[] } { + return isDevtoolsMessage(message) && message.type === MessageType.RPCRequest; +} const ports: Record< number, { - tabPorts: Set; + tabPorts: Map; + clientFrames: Map; extension: browser.Runtime.Port | null; disconnectPorts: (() => void) | null; } @@ -17,18 +38,28 @@ const ports: Record< function registerTab(tabId: number) { if (!ports[tabId]) { ports[tabId] = { - tabPorts: new Set(), + tabPorts: new Map(), + clientFrames: new Map(), extension: null, disconnectPorts: null, }; } } -function registerTabPort(tabId: number, port: browser.Runtime.Port) { - ports[tabId].tabPorts.add(port); +function registerTabPort( + tabId: number, + port: browser.Runtime.Port, + frameId: number +) { + ports[tabId].tabPorts.set(port, frameId); port.onDisconnect.addListener(() => { ports[tabId].tabPorts.delete(port); + for (const [clientId, fId] of ports[tabId].clientFrames) { + if (fId === frameId) { + ports[tabId].clientFrames.delete(clientId); + } + } if (ports[tabId].tabPorts.size === 0) { ports[tabId].disconnectPorts?.(); } @@ -53,6 +84,7 @@ function connectPorts(tabId: number) { const extensionPort = ports[tabId].extension; const tabPorts = ports[tabId].tabPorts; + const clientFrames = ports[tabId].clientFrames; if (!extensionPort) { throw new Error("Attempted to connect extension port which does not exist"); @@ -74,8 +106,18 @@ function connectPorts(tabId: number) { >(); function extensionPortListener(message: unknown) { - // Broadcast to ALL tab ports (main window + iframes) - for (const tabPort of tabPorts) { + let targetFrameId: number | undefined; + if (isRPCRequestMessage(message)) { + const clientId = message.params[0]; + if (typeof clientId === "string") { + targetFrameId = clientFrames.get(clientId); + } + } + + for (const [tabPort, frameId] of tabPorts) { + if (targetFrameId !== undefined && frameId !== targetFrameId) { + continue; + } try { tabPort.postMessage(message); } catch (e) { @@ -87,8 +129,21 @@ function connectPorts(tabId: number) { } } - function createTabPortListener() { + function createTabPortListener(frameId: number) { return function tabPortListener(message: unknown) { + if (isActorMessage(message)) { + if ( + message.message.type === "registerClient" && + message.message.payload?.id + ) { + clientFrames.set(message.message.payload.id, frameId); + } else if ( + message.message.type === "clientTerminated" && + message.message.clientId + ) { + clientFrames.delete(message.message.clientId); + } + } try { extensionPort!.postMessage(message); } catch (e) { @@ -114,7 +169,7 @@ function connectPorts(tabId: number) { extensionPort.onMessage.addListener(extensionPortListener); extensionPort.onDisconnect.addListener(disconnectPorts); extensionPort.onDisconnect.addListener(() => { - for (const tabPort of tabPorts) { + for (const [tabPort] of tabPorts) { tabPort.postMessage( createDevtoolsMessage({ type: MessageType.Actor, @@ -125,8 +180,8 @@ function connectPorts(tabId: number) { }); // Add listeners for all current tab ports - for (const tabPort of tabPorts) { - const listener = createTabPortListener(); + for (const [tabPort, frameId] of tabPorts) { + const listener = createTabPortListener(frameId); tabPortListeners.set(tabPort, listener); tabPort.onMessage.addListener(listener); tabPort.onDisconnect.addListener(() => { @@ -145,9 +200,10 @@ function connectTabPort(port: browser.Runtime.Port) { } const tabId = port.sender.tab.id; + const frameId = port.sender.frameId ?? 0; registerTab(tabId); - registerTabPort(tabId, port); + registerTabPort(tabId, port, frameId); if (ports[tabId].extension) { connectPorts(tabId); @@ -155,7 +211,21 @@ function connectTabPort(port: browser.Runtime.Port) { if (ports[tabId].disconnectPorts) { // Need to add message listener for this new tab port const extensionPort = ports[tabId].extension!; + const clientFrames = ports[tabId].clientFrames; const listener = (message: unknown) => { + if (isActorMessage(message)) { + if ( + message.message.type === "registerClient" && + message.message.payload?.id + ) { + clientFrames.set(message.message.payload.id, frameId); + } else if ( + message.message.type === "clientTerminated" && + message.message.clientId + ) { + clientFrames.delete(message.message.clientId); + } + } try { extensionPort.postMessage(message); } catch (e) { From 1374d7fd1ca0915a226f2464a51630418bc90344 Mon Sep 17 00:00:00 2001 From: Camille Lawrence Date: Wed, 28 Jan 2026 16:48:54 -0500 Subject: [PATCH 4/6] refactor(hook): remove SKIP_RESPONSE from targeted handlers --- src/extension/tab/hook.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/extension/tab/hook.ts b/src/extension/tab/hook.ts index 49e2b5b79..1c60c0ca7 100755 --- a/src/extension/tab/hook.ts +++ b/src/extension/tab/hook.ts @@ -82,50 +82,50 @@ handleRpc("getClients", () => { handleRpc("getClient", (clientId) => { const handler = getHandlerByClientId(clientId as any); if (!handler) { - return SKIP_RESPONSE as any; + return null; } return getClientInfo(handler.getClient()); }); handleRpc("getV3Queries", (clientId) => { const handler = getHandlerByClientId(clientId); - if (!handler) return SKIP_RESPONSE as any; + if (!handler) return []; return handler.getQueries(); }); handleRpc("getV4Queries", (clientId) => { const handler = getHandlerByClientId(clientId); - if (!handler) return SKIP_RESPONSE as any; + if (!handler) return []; return handler.getQueries(); }); handleRpc("getV3Mutations", (clientId) => { const handler = getHandlerByClientId(clientId); - if (!handler) return SKIP_RESPONSE as any; + if (!handler) return []; return handler.getMutations(); }); handleRpc("getV4Mutations", (clientId) => { const handler = getHandlerByClientId(clientId); - if (!handler) return SKIP_RESPONSE as any; + if (!handler) return []; return handler.getMutations(); }); handleRpc("getCache", (clientId) => { const client = getClientById(clientId as any); - if (!client) return SKIP_RESPONSE as any; + if (!client) return {}; return client.cache.extract(true) as JSONObject; }); handleRpc("getV3MemoryInternals", (clientId) => { const client = getClientById(clientId); - if (!client) return SKIP_RESPONSE as any; + if (!client) return undefined; return client.getMemoryInternals?.(); }); handleRpc("getV4MemoryInternals", (clientId) => { const client = getClientById(clientId); - if (!client) return SKIP_RESPONSE as any; + if (!client) return undefined; return client.getMemoryInternals?.(); }); From c817db0a8840b8b066fd37444adc5aa1717dfafe Mon Sep 17 00:00:00 2001 From: Camille Lawrence Date: Wed, 28 Jan 2026 17:12:06 -0500 Subject: [PATCH 5/6] test(rpc): add test coverage for SKIP_RESPONSE --- src/extension/__tests__/rpc.test.ts | 42 +++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/src/extension/__tests__/rpc.test.ts b/src/extension/__tests__/rpc.test.ts index ac193c21c..c9cd24ccb 100644 --- a/src/extension/__tests__/rpc.test.ts +++ b/src/extension/__tests__/rpc.test.ts @@ -15,6 +15,7 @@ import { createRpcClient, createRpcHandler, createRpcStreamHandler, + SKIP_RESPONSE, } from "../rpc"; type RPCMessage = RPCRequestMessage | RPCResponseMessage; @@ -923,3 +924,44 @@ test("runs cleanup and closes stream when calling close", async () => { done: true, }); }); + +test("does not send response when handler returns SKIP_RESPONSE", async () => { + const handlerAdapter = createTestAdapter(); + + const handle = createRpcHandler(handlerAdapter); + handle("getClient", () => SKIP_RESPONSE as any); + + // Simulate an RPC request + handlerAdapter.simulateRPCMessage({ + id: "abc", + type: MessageType.RPCRequest, + name: "getClient", + params: ["1"], + }); + + // Give time for any response to be sent + await wait(10); + + // No response should have been posted + expect(handlerAdapter.postMessage).not.toHaveBeenCalled(); +}); + +test("SKIP_RESPONSE allows handler to be re-registered after unsubscribe", async () => { + const handlerAdapter = createTestAdapter(); + const clientAdapter = createTestAdapter(); + createBridge(clientAdapter, handlerAdapter); + + const client = createRpcClient(clientAdapter); + const handle = createRpcHandler(handlerAdapter); + + // First handler skips + const unsubscribe = handle("getClient", () => SKIP_RESPONSE as any); + + unsubscribe(); + + // Re-register with a real handler + handle("getClient", defaultGetClient); + + const result = await client.request("getClient", "1"); + expect(result).toEqual(defaultGetClient("1")); +}); From 26712e0161a8068eef957fe5f0d23de2a72a84c1 Mon Sep 17 00:00:00 2001 From: Camille Lawrence Date: Wed, 28 Jan 2026 17:20:26 -0500 Subject: [PATCH 6/6] fix formatting issues --- src/extension/background/background.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/extension/background/background.ts b/src/extension/background/background.ts index 22f69b3be..e15b53f3d 100644 --- a/src/extension/background/background.ts +++ b/src/extension/background/background.ts @@ -10,9 +10,7 @@ import { } from "../messages"; // Type guards for message routing -function isActorMessage( - message: unknown -): message is { +function isActorMessage(message: unknown): message is { type: MessageType.Actor; message: { type: string; payload?: { id?: string }; clientId?: string }; } {