From 63e0d1f705d5dcd8f482b3983d72b2b2ab85a2ba Mon Sep 17 00:00:00 2001 From: abhinav7x94 Date: Sun, 16 Aug 2026 07:12:06 +0530 Subject: [PATCH] fix(extension): keep X import status on initiating tab --- .../entrypoints/background.ts | 47 +++-------- .../twitter-import-notifications.test.ts | 77 +++++++++++++++++++ .../utils/twitter-import-notifications.ts | 50 ++++++++++++ 3 files changed, 138 insertions(+), 36 deletions(-) create mode 100644 apps/browser-extension/utils/twitter-import-notifications.test.ts create mode 100644 apps/browser-extension/utils/twitter-import-notifications.ts diff --git a/apps/browser-extension/entrypoints/background.ts b/apps/browser-extension/entrypoints/background.ts index ccf3dce0..dfff5680 100644 --- a/apps/browser-extension/entrypoints/background.ts +++ b/apps/browser-extension/entrypoints/background.ts @@ -15,6 +15,7 @@ import { type TwitterImportConfig, TwitterImporter, } from "../utils/twitter-import" +import { createTwitterImportNotifications } from "../utils/twitter-import-notifications" import type { ExtensionMessage, MemoryData, @@ -90,36 +91,6 @@ export default defineBackground(() => { ["requestHeaders", "extraHeaders"], ) - // Send message to current active tab. - const sendMessageToCurrentTab = async (message: string) => { - const tabs = await browser.tabs.query({ - active: true, - currentWindow: true, - }) - if (tabs.length > 0 && tabs[0].id) { - await browser.tabs.sendMessage(tabs[0].id, { - type: MESSAGE_TYPES.IMPORT_UPDATE, - importedMessage: message, - }) - } - } - - /** - * Send import completion message - */ - const sendImportDoneMessage = async (totalImported: number) => { - const tabs = await browser.tabs.query({ - active: true, - currentWindow: true, - }) - if (tabs.length > 0 && tabs[0].id) { - await browser.tabs.sendMessage(tabs[0].id, { - type: MESSAGE_TYPES.IMPORT_DONE, - totalImported, - }) - } - } - /** * Save memory to supermemory API */ @@ -246,18 +217,21 @@ export default defineBackground(() => { * Handle extension messages */ browser.runtime.onMessage.addListener( - (message: ExtensionMessage, _sender, sendResponse) => { + (message: ExtensionMessage, sender, sendResponse) => { // Handle Twitter import request if (message.type === MESSAGE_TYPES.BATCH_IMPORT_ALL) { + const notifications = createTwitterImportNotifications( + sender.tab?.id, + (tabId, notification) => + browser.tabs.sendMessage(tabId, notification), + ) const importConfig: TwitterImportConfig = { isFolderImport: message.isFolderImport, bookmarkCollectionId: message.bookmarkCollectionId, selectedProject: message.selectedProject, - onProgress: sendMessageToCurrentTab, - onComplete: sendImportDoneMessage, - onError: async (error: Error) => { - await sendMessageToCurrentTab(`Error: ${error.message}`) - }, + onProgress: notifications.onProgress, + onComplete: notifications.onComplete, + onError: notifications.onError, } twitterImporter = new TwitterImporter(importConfig) @@ -351,3 +325,4 @@ export default defineBackground(() => { }, ) }) + diff --git a/apps/browser-extension/utils/twitter-import-notifications.test.ts b/apps/browser-extension/utils/twitter-import-notifications.test.ts new file mode 100644 index 00000000..3003dc89 --- /dev/null +++ b/apps/browser-extension/utils/twitter-import-notifications.test.ts @@ -0,0 +1,77 @@ +import { describe, expect, mock, test } from "bun:test" +import { MESSAGE_TYPES } from "./constants" +import { createTwitterImportNotifications } from "./twitter-import-notifications" + +describe("Twitter import notifications", () => { + test("keeps progress, errors, and completion on the initiating tab", async () => { + let activeTabId = 7 + const sendMessage = mock(async () => {}) + const notifications = createTwitterImportNotifications( + activeTabId, + sendMessage, + ) + + activeTabId = 42 + await notifications.onProgress("Imported 10 bookmarks") + await notifications.onError(new Error("rate limited")) + await notifications.onComplete(10) + + expect(activeTabId).toBe(42) + expect(sendMessage.mock.calls).toEqual([ + [ + 7, + { + type: MESSAGE_TYPES.IMPORT_UPDATE, + importedMessage: "Imported 10 bookmarks", + }, + ], + [ + 7, + { + type: MESSAGE_TYPES.IMPORT_UPDATE, + importedMessage: "Error: rate limited", + }, + ], + [7, { type: MESSAGE_TYPES.IMPORT_DONE, totalImported: 10 }], + ]) + }) + + test("does not let tab closure interrupt import callbacks", async () => { + const sendMessage = mock(async () => { + throw new Error("Receiving end does not exist") + }) + const notifications = createTwitterImportNotifications(7, sendMessage) + + await notifications.onProgress("Retrying") + await notifications.onError(new Error("failed")) + await notifications.onComplete(0) + + expect(sendMessage).toHaveBeenCalledTimes(3) + }) + + test("accepts tab id zero", async () => { + const sendMessage = mock(async () => {}) + const notifications = createTwitterImportNotifications(0, sendMessage) + + await notifications.onProgress("Starting") + + expect(sendMessage).toHaveBeenCalledWith(0, { + type: MESSAGE_TYPES.IMPORT_UPDATE, + importedMessage: "Starting", + }) + }) + + test("skips notifications when the request has no sender tab", async () => { + const sendMessage = mock(async () => {}) + const notifications = createTwitterImportNotifications( + undefined, + sendMessage, + ) + + await notifications.onProgress("Starting") + await notifications.onComplete(0) + + expect(sendMessage).not.toHaveBeenCalled() + }) +}) + diff --git a/apps/browser-extension/utils/twitter-import-notifications.ts b/apps/browser-extension/utils/twitter-import-notifications.ts new file mode 100644 index 00000000..dc08b8d0 --- /dev/null +++ b/apps/browser-extension/utils/twitter-import-notifications.ts @@ -0,0 +1,50 @@ +import { MESSAGE_TYPES } from "./constants" + +type TwitterImportNotification = + | { + type: typeof MESSAGE_TYPES.IMPORT_UPDATE + importedMessage: string + } + | { + type: typeof MESSAGE_TYPES.IMPORT_DONE + totalImported: number + } + +type SendTabMessage = ( + tabId: number, + message: TwitterImportNotification, +) => Promise + +export function createTwitterImportNotifications( + tabId: number | undefined, + sendMessage: SendTabMessage, +) { + const deliver = async (message: TwitterImportNotification): Promise => { + if (tabId === undefined) return + try { + await sendMessage(tabId, message) + } catch { + // The initiating tab can be closed or navigated while the import keeps + // running. Notification delivery must not cancel the import itself. + } + } + + return { + onProgress: (message: string) => + deliver({ + type: MESSAGE_TYPES.IMPORT_UPDATE, + importedMessage: message, + }), + onComplete: (totalImported: number) => + deliver({ + type: MESSAGE_TYPES.IMPORT_DONE, + totalImported, + }), + onError: (error: Error) => + deliver({ + type: MESSAGE_TYPES.IMPORT_UPDATE, + importedMessage: `Error: ${error.message}`, + }), + } +} +