fix(extension): keep X import status on initiating tab

This commit is contained in:
abhinav7x94 2026-08-16 07:12:06 +05:30
parent e651045ac5
commit 63e0d1f705
3 changed files with 138 additions and 36 deletions

View file

@ -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(() => {
},
)
})

View file

@ -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()
})
})

View file

@ -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<unknown>
export function createTwitterImportNotifications(
tabId: number | undefined,
sendMessage: SendTabMessage,
) {
const deliver = async (message: TwitterImportNotification): Promise<void> => {
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}`,
}),
}
}