From 61698911fc85647a8a63510028befff078435a3f Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 28 Feb 2026 19:21:29 +0000 Subject: [PATCH 1/2] Fix bang redirects on cold service worker startup Co-authored-by: ShoeBoom --- apps/extension/src/entrypoints/background.ts | 63 +++++++++++--------- apps/extension/src/utils/storage.ts | 24 ++++++-- 2 files changed, 54 insertions(+), 33 deletions(-) diff --git a/apps/extension/src/entrypoints/background.ts b/apps/extension/src/entrypoints/background.ts index 03aa06a..ed474a1 100644 --- a/apps/extension/src/entrypoints/background.ts +++ b/apps/extension/src/entrypoints/background.ts @@ -114,37 +114,42 @@ function buildBangUrl(bang: BangsData["bangs"][number], searchQuery: string) { } const addBangsListener = (props: { - quickBangs: () => string[]; - aliases: () => BangAliases; - active: () => boolean; + quickBangs: () => Promise; + aliases: () => Promise; + active: () => Promise; }) => { return browser.webRequest.onBeforeRequest.addListener( (details): undefined => { - if (props.active() === false) return; if (details.frameId !== 0) return; - try { - const url = new URL(details.url); - const query = url.searchParams.get("q"); - - if (!query) return; - - const quickBangs = props.quickBangs(); - const aliases = props.aliases(); - const bang = parseBang(query, quickBangs, aliases); - if (!bang) return; - - const redirectUrl = buildBangUrl(bang.data, bang.searchQuery); - if (redirectUrl) { - console.log( - `[SearchTuner] Bang redirect: ${bang.match} -> ${redirectUrl}`, - ); - - // Redirect the tab - browser.tabs.update(details.tabId, { url: redirectUrl }); + void (async () => { + try { + const url = new URL(details.url); + const query = url.searchParams.get("q"); + if (!query) return; + + const isActive = await props.active(); + if (isActive === false) return; + + const [quickBangs, aliases] = await Promise.all([ + props.quickBangs(), + props.aliases(), + ]); + const bang = parseBang(query, quickBangs, aliases); + if (!bang) return; + + const redirectUrl = buildBangUrl(bang.data, bang.searchQuery); + if (redirectUrl && details.tabId >= 0) { + console.log( + `[SearchTuner] Bang redirect: ${bang.match} -> ${redirectUrl}`, + ); + + // Redirect the tab + await browser.tabs.update(details.tabId, { url: redirectUrl }); + } + } catch (error) { + console.error("[SearchTuner] Error processing bang:", error); } - } catch (error) { - console.error("[SearchTuner] Error processing bang:", error); - } + })(); }, { urls: googleSearchPatterns, types: ["main_frame"] }, ); @@ -156,8 +161,8 @@ export default defineBackground(() => { const aliasesObserver = observeItem(items.bang_aliases, {} as BangAliases); addBangsListener({ - quickBangs: quickBangsObserver.get, - aliases: aliasesObserver.get, - active: activeObserver.get, + quickBangs: quickBangsObserver.getAsync, + aliases: aliasesObserver.getAsync, + active: activeObserver.getAsync, }); }); diff --git a/apps/extension/src/utils/storage.ts b/apps/extension/src/utils/storage.ts index bf12eee..760e6fa 100644 --- a/apps/extension/src/utils/storage.ts +++ b/apps/extension/src/utils/storage.ts @@ -91,6 +91,7 @@ type StorageItem< type ItemObserver = { get: () => T; + getAsync: () => Promise; unsubscribe: () => void; }; @@ -99,17 +100,32 @@ export const observeItem = ( fallback: T, ): ItemObserver => { let current = fallback; - - void itemDef.getValue().then((initialValue) => { - current = initialValue ?? fallback; - }); + let loaded = false; + + const initialLoad = itemDef + .getValue() + .then((initialValue) => { + current = initialValue ?? fallback; + loaded = true; + return current; + }) + .catch((error) => { + loaded = true; + console.error("[SearchTuner] Failed to hydrate storage item:", error); + return current; + }); const unsubscribe = itemDef.watch((newVal) => { current = newVal ?? fallback; + loaded = true; }); return { get: () => current, + getAsync: async () => { + if (!loaded) await initialLoad; + return current; + }, unsubscribe, }; }; From d3651bc1672063fc8d0098685fea410c68c7d82e Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 28 Feb 2026 19:31:40 +0000 Subject: [PATCH 2/2] Harden MV3 background wake-up for bang redirects Co-authored-by: ShoeBoom --- apps/extension/src/entrypoints/background.ts | 144 ++++++++++++------ .../src/entrypoints/content/index.tsx | 11 +- 2 files changed, 105 insertions(+), 50 deletions(-) diff --git a/apps/extension/src/entrypoints/background.ts b/apps/extension/src/entrypoints/background.ts index ed474a1..0b5568a 100644 --- a/apps/extension/src/entrypoints/background.ts +++ b/apps/extension/src/entrypoints/background.ts @@ -1,9 +1,10 @@ import type { BangsData } from "@searchtuner/bangs/types"; import { browser, defineBackground } from "#imports"; import { getGoogleDomains } from "@/assets/googledomains"; -import { type BangAliases, getBang, items, observeItem } from "@/utils/storage"; +import { type BangAliases, getBang, items } from "@/utils/storage"; const googleSearchPatterns = getGoogleDomains(); +const GOOGLE_SEARCH_PING = "searchtuner:google-search"; // Parse bang from query - supports "!w query", "query !w", and quick bangs at start/end function parseBang( @@ -113,56 +114,101 @@ function buildBangUrl(bang: BangsData["bangs"][number], searchQuery: string) { return url; } -const addBangsListener = (props: { - quickBangs: () => Promise; - aliases: () => Promise; - active: () => Promise; +const getBangConfig = async () => { + const [active, quickBangs, aliases] = await Promise.all([ + items.bangs_active.getValue(), + items.quick_bangs.getValue(), + items.bang_aliases.getValue(), + ]); + return { + active: active ?? false, + quickBangs: quickBangs ?? [], + aliases: aliases ?? ({} as BangAliases), + }; +}; + +const processBangForRequest = async (details: { + url: string; + tabId: number; + frameId: number; }) => { - return browser.webRequest.onBeforeRequest.addListener( - (details): undefined => { - if (details.frameId !== 0) return; - void (async () => { - try { - const url = new URL(details.url); - const query = url.searchParams.get("q"); - if (!query) return; - - const isActive = await props.active(); - if (isActive === false) return; - - const [quickBangs, aliases] = await Promise.all([ - props.quickBangs(), - props.aliases(), - ]); - const bang = parseBang(query, quickBangs, aliases); - if (!bang) return; - - const redirectUrl = buildBangUrl(bang.data, bang.searchQuery); - if (redirectUrl && details.tabId >= 0) { - console.log( - `[SearchTuner] Bang redirect: ${bang.match} -> ${redirectUrl}`, - ); - - // Redirect the tab - await browser.tabs.update(details.tabId, { url: redirectUrl }); - } - } catch (error) { - console.error("[SearchTuner] Error processing bang:", error); - } - })(); - }, - { urls: googleSearchPatterns, types: ["main_frame"] }, - ); + if (details.frameId !== 0 || details.tabId < 0) return; + + try { + const url = new URL(details.url); + const query = url.searchParams.get("q"); + if (!query) return; + + const config = await getBangConfig(); + if (config.active === false) return; + + const bang = parseBang(query, config.quickBangs, config.aliases); + if (!bang) return; + + const redirectUrl = buildBangUrl(bang.data, bang.searchQuery); + if (!redirectUrl) return; + + console.log(`[SearchTuner] Bang redirect: ${bang.match} -> ${redirectUrl}`); + await browser.tabs.update(details.tabId, { url: redirectUrl }); + } catch (error) { + console.error("[SearchTuner] Error processing bang:", error); + } }; -export default defineBackground(() => { - const activeObserver = observeItem(items.bangs_active, false); - const quickBangsObserver = observeItem(items.quick_bangs, [] as string[]); - const aliasesObserver = observeItem(items.bang_aliases, {} as BangAliases); +type GoogleSearchPingMessage = { + type: typeof GOOGLE_SEARCH_PING; + url?: string; +}; + +const isGoogleSearchPingMessage = ( + message: unknown, +): message is GoogleSearchPingMessage => { + if (typeof message !== "object" || message === null) return false; + if (!("type" in message)) return false; + return message.type === GOOGLE_SEARCH_PING; +}; - addBangsListener({ - quickBangs: quickBangsObserver.getAsync, - aliases: aliasesObserver.getAsync, - active: activeObserver.getAsync, +const onBeforeRequest: Parameters< + typeof browser.webRequest.onBeforeRequest.addListener +>[0] = (details): undefined => { + void processBangForRequest({ + url: details.url, + tabId: details.tabId, + frameId: details.frameId, }); -}); + return; +}; + +const onMessage: Parameters[0] = ( + message, + sender, +): void => { + if (!isGoogleSearchPingMessage(message)) return; + + const tabId = sender.tab?.id; + if (typeof tabId !== "number" || tabId < 0) return; + + const requestedUrl = + typeof message.url === "string" ? message.url : sender.tab?.url; + if (!requestedUrl) return; + + void processBangForRequest({ + url: requestedUrl, + tabId, + frameId: 0, + }); +}; + +// Register listeners at module top-level so MV3 can wake the worker reliably. +try { + browser.webRequest.onBeforeRequest.addListener(onBeforeRequest, { + urls: googleSearchPatterns, + types: ["main_frame"], + }); +} catch {} + +try { + browser.runtime.onMessage.addListener(onMessage); +} catch {} + +export default defineBackground(() => {}); diff --git a/apps/extension/src/entrypoints/content/index.tsx b/apps/extension/src/entrypoints/content/index.tsx index 359a466..5012484 100644 --- a/apps/extension/src/entrypoints/content/index.tsx +++ b/apps/extension/src/entrypoints/content/index.tsx @@ -1,6 +1,6 @@ import $ from "jquery"; import { render } from "solid-js/web"; -import { defineContentScript } from "#imports"; +import { browser, defineContentScript } from "#imports"; import { getGoogleDomains } from "@/assets/googledomains"; import Popup from "@/entrypoints/content/components/popup"; import { getResults, type Results } from "@/utils/filter"; @@ -12,6 +12,7 @@ const RERANK_WEIGHTS = { normal: 3, strong: 5, } as const; +const GOOGLE_SEARCH_PING = "searchtuner:google-search"; function orderedResults(results: Results, rankings: RankingsV2 | null) { const totalResults = results.length; @@ -151,6 +152,14 @@ export default defineContentScript({ matches: getGoogleDomains(), runAt: "document_start", main() { + // Wake background on each Google search page as a fallback path. + void browser.runtime + .sendMessage({ + type: GOOGLE_SEARCH_PING, + url: window.location.href, + }) + .catch(() => undefined); + hideMain(); const configPromise = getConfig(); // backup to show main if the config is not active