diff --git a/src/background/executor.ts b/src/background/executor.ts index 51e55b5923..a3010599e7 100644 --- a/src/background/executor.ts +++ b/src/background/executor.ts @@ -28,13 +28,36 @@ import { runBrick } from "@/contentScript/messenger/api"; import { Target } from "@/types"; import { RemoteExecutionError } from "@/blocks/errors"; import pDefer from "p-defer"; +import { canAccessTab } from "webext-tools"; +import { onTabClose } from "@/chrome"; type TabId = number; +// Used to determine which promise was resolved in a race +const TYPE_WAS_CLOSED = Symbol("Tab was closed"); + const tabToOpener = new Map(); const tabToTarget = new Map(); // TODO: One tab could have multiple targets, but `tabToTarget` currenly only supports one at a time +async function safelyRunBrick({ tabId }: { tabId: number }, request: RunBlock) { + if (!(await canAccessTab(tabId))) { + throw new BusinessError("PixieBrix doesn't have access to the tab"); + } + + const result = await Promise.race([ + // If https://github.com/pixiebrix/webext-messenger/issues/67 is resolved, we don't need the listener + onTabClose(tabId).then(() => TYPE_WAS_CLOSED), + runBrick({ tabId }, request), + ]); + + if (result === TYPE_WAS_CLOSED) { + throw new BusinessError("The tab was closed"); + } + + return result; +} + export async function waitForTargetByUrl(url: string): Promise { const { promise, resolve } = pDefer(); @@ -65,7 +88,7 @@ export async function requestRunInOpener( tabId: tabToOpener.get(sourceTabId), }; const subRequest = { ...request, sourceTabId }; - return runBrick(opener, subRequest); + return safelyRunBrick(opener, subRequest); } export async function requestRunInBroadcast( @@ -87,7 +110,7 @@ export async function requestRunInBroadcast( } try { - const response = runBrick({ tabId: tab.id }, subRequest); + const response = safelyRunBrick({ tabId: tab.id }, subRequest); fulfilled.set(tab.id, await response); } catch (error) { rejected.set(tab.id, error); @@ -113,7 +136,7 @@ export async function requestRunInTarget( } const subRequest = { ...request, sourceTabId }; - return runBrick({ tabId: target }, subRequest); + return safelyRunBrick({ tabId: target }, subRequest); } export async function openTab( diff --git a/src/chrome.ts b/src/chrome.ts index b9d9ca9adf..af8a88fb17 100644 --- a/src/chrome.ts +++ b/src/chrome.ts @@ -138,3 +138,16 @@ export async function setReduxStorage( ): Promise { await browser.storage.local.set({ [storageKey]: JSON.stringify(value) }); } + +export async function onTabClose(watchedTabId: number): Promise { + await new Promise((resolve) => { + const listener = (closedTabId: number) => { + if (closedTabId === watchedTabId) { + resolve(); + browser.tabs.onRemoved.removeListener(listener); + } + }; + + browser.tabs.onRemoved.addListener(listener); + }); +} diff --git a/src/errors.ts b/src/errors.ts index 5ff0e987ba..1ad538e92a 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -174,9 +174,11 @@ export class ContextError extends Error { } } +export const NO_TARGET_FOUND_CONNECTION_ERROR = + "Could not establish connection. Receiving end does not exist."; /** Browser Messenger API error message patterns */ export const CONNECTION_ERROR_MESSAGES = [ - "Could not establish connection. Receiving end does not exist.", + NO_TARGET_FOUND_CONNECTION_ERROR, "Extension context invalidated.", ];