refactor: migrate from webext-core/messaging to comctx

Replace @webext-core/messaging with comctx for extension
messaging. Implement service-based architecture for AppAction
and Notification with runtime message adapters.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
molvqingtai
2025-10-01 19:02:51 +08:00
parent 38e10baebf
commit 9f63ed266f
13 changed files with 171 additions and 92 deletions

View File

@@ -65,15 +65,16 @@
"@rtco/client": "^0.3.6",
"@tailwindcss/typography": "^0.5.19",
"@webcomponents/custom-elements": "^1.6.0",
"@webext-core/messaging": "^2.3.0",
"@webext-core/proxy-service": "^1.2.1",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cobe": "^0.6.5",
"comctx": "^1.4.3",
"danmu": "^0.18.1",
"date-fns": "^4.1.0",
"framer-motion": "^12.23.22",
"idb-keyval": "^6.2.2",
"imgcap": "^1.0.2",
"lucide-react": "^0.544.0",
"motion": "^12.23.22",
"nanoid": "^5.1.6",

19
pnpm-lock.yaml generated
View File

@@ -71,9 +71,6 @@ importers:
'@webcomponents/custom-elements':
specifier: ^1.6.0
version: 1.6.0
'@webext-core/messaging':
specifier: ^2.3.0
version: 2.3.0
'@webext-core/proxy-service':
specifier: ^1.2.1
version: 1.2.1(@webext-core/messaging@2.3.0)(webextension-polyfill@0.12.0)
@@ -86,6 +83,9 @@ importers:
cobe:
specifier: ^0.6.5
version: 0.6.5
comctx:
specifier: ^1.4.3
version: 1.4.3
danmu:
specifier: ^0.18.1
version: 0.18.1
@@ -98,6 +98,9 @@ importers:
idb-keyval:
specifier: ^6.2.2
version: 6.2.2
imgcap:
specifier: ^1.0.2
version: 1.0.2
lucide-react:
specifier: ^0.544.0
version: 0.544.0(react@19.1.1)
@@ -2183,6 +2186,9 @@ packages:
colorette@2.0.20:
resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==}
comctx@1.4.3:
resolution: {integrity: sha512-Z1fisV3l6kucW+GZDg3AI2+ffWGd+Q/RCBKGbpQ8oe6U3p6wmTM/NciP4RRgLYYDW4aZgZIXRUQz1YnFX1QsjQ==}
comma-separated-tokens@2.0.3:
resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==}
@@ -3154,6 +3160,9 @@ packages:
resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==}
engines: {node: '>= 4'}
imgcap@1.0.2:
resolution: {integrity: sha512-KY04sGVRupPrRBIruSBDnBBEe/5iwyivnukzmhOBclIkdyMJcbfrlqZrSZjQi1254NJXxOtpB3ddmHUjdKNydg==}
immediate@3.0.6:
resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==}
@@ -7796,6 +7805,8 @@ snapshots:
colorette@2.0.20: {}
comctx@1.4.3: {}
comma-separated-tokens@2.0.3: {}
commander@14.0.1: {}
@@ -8959,6 +8970,8 @@ snapshots:
ignore@7.0.5: {}
imgcap@1.0.2: {}
immediate@3.0.6: {}
import-fresh@3.3.1:

View File

@@ -1,69 +1,23 @@
import { EVENT } from '@/constants/event'
import { messenger } from '@/messenger'
import { browser, defineBackground } from '#imports'
import type { Browser } from 'wxt/browser'
import { ProvideAdapter } from '@/service/adapter/runtimeMessage'
import { defineProxy } from 'comctx'
import { AppAction } from '@/service/AppAction'
import { Notification } from '@/service/Notification'
export default defineBackground({
type: 'module',
main() {
browser.action.onClicked.addListener(() => {
browser.runtime.openOptionsPage()
const [provideNotification] = defineProxy(() => new Notification(), {
namespace: browser.runtime.id
})
const [provideAppAction] = defineProxy(() => new AppAction(), {
namespace: browser.runtime.id
})
const historyNotificationTabs = new Map<string, Browser.tabs.Tab>()
messenger.onMessage(EVENT.OPTIONS_PAGE_OPEN, () => {
browser.runtime.openOptionsPage()
})
provideNotification(new ProvideAdapter())
messenger.onMessage(EVENT.NOTIFICATION_PUSH, async ({ data: message, sender }) => {
// Check if there is an active tab on the same site
const tabs = await browser.tabs.query({ active: true })
const hasActiveSomeSiteTab = tabs.some((tab) => {
return new URL(tab.url!).origin === new URL(sender.tab!.url!).origin
})
const appAction = provideAppAction(new ProvideAdapter())
if (hasActiveSomeSiteTab) return
browser.notifications.create(message.id, {
type: 'basic',
iconUrl: message.userAvatar,
title: message.username,
message: message.body,
contextMessage: sender.tab!.url!
})
historyNotificationTabs.set(message.id, sender.tab! as Browser.tabs.Tab)
})
messenger.onMessage(EVENT.NOTIFICATION_CLEAR, async ({ data: id }) => {
browser.notifications.clear(id)
})
browser.notifications.onButtonClicked.addListener(async (id) => {
const fromTab = historyNotificationTabs.get(id)
if (fromTab?.id) {
try {
const tab = await browser.tabs.get(fromTab.id)
browser.tabs.update(tab.id!, { active: true, highlighted: true })
browser.windows.update(tab.windowId!, { focused: true })
} catch {
browser.tabs.create({ url: fromTab.url })
}
}
})
browser.notifications.onClicked.addListener(async (id) => {
const fromTab = historyNotificationTabs.get(id)
if (fromTab?.id) {
try {
const tab = await browser.tabs.get(fromTab.id)
browser.tabs.update(tab.id!, { active: true })
} catch {
browser.tabs.create({ url: fromTab.url })
}
}
})
browser.notifications.onClosed.addListener(async (id) => {
historyNotificationTabs.delete(id)
})
browser.action.onClicked.addListener(() => appAction.openOptionsPage())
}
})

View File

@@ -4,7 +4,6 @@ import { motion, AnimatePresence } from 'framer-motion'
import { useRemeshDomain, useRemeshQuery, useRemeshSend } from 'remesh-react'
import { Button } from '@/components/ui/button'
import { EVENT } from '@/constants/event'
import UserInfoDomain from '@/domain/UserInfo'
import useTriggerAway from '@/hooks/useTriggerAway'
import { checkDarkMode, cn } from '@/utils'
@@ -17,9 +16,9 @@ import LogoIcon5 from '@/assets/images/logo-5.svg'
import LogoIcon6 from '@/assets/images/logo-6.svg'
import AppStatusDomain from '@/domain/AppStatus'
import { getDay } from 'date-fns'
import { messenger } from '@/messenger'
import useDraggable from '@/hooks/useDraggable'
import useWindowResize from '@/hooks/useWindowResize'
import { AppActionImpl } from '@/domain/impls/AppAction'
export interface AppButtonProps {
className?: string
@@ -80,7 +79,7 @@ const AppButton: FC<AppButtonProps> = ({ className }) => {
}
const handleOpenOptionsPage = () => {
messenger.sendMessage(EVENT.OPTIONS_PAGE_OPEN, undefined)
AppActionImpl.value.openOptionsPage()
}
const handleToggleApp = () => {

View File

@@ -1,6 +1,3 @@
export enum EVENT {
OPTIONS_PAGE_OPEN = `WEB_CHAT_OPTIONS_PAGE_OPEN`,
APP_OPEN = 'WEB_CHAT_APP_OPEN',
NOTIFICATION_PUSH = 'WEB_CHAT_NOTIFICATION_PUSH',
NOTIFICATION_CLEAR = 'WEB_CHAT_NOTIFICATION_CLEAR'
APP_OPEN = 'WEB_CHAT_APP_OPEN'
}

View File

@@ -0,0 +1,13 @@
import { Remesh } from 'remesh'
export interface AppAction {
openOptionsPage: () => Promise<void>
}
export const AppActionExtern = Remesh.extern<AppAction>({
default: {
openOptionsPage: () => {
throw new Error('"openOptionsPage" not implemented.')
}
}
})

View File

@@ -2,7 +2,7 @@ import { Remesh } from 'remesh'
import type { TextMessage } from '@/domain/ChatRoom'
export interface Notification {
push: (message: TextMessage) => Promise<string>
push: (message: TextMessage) => Promise<string | void>
}
export const NotificationExtern = Remesh.extern<Notification>({

View File

@@ -0,0 +1,13 @@
import { browser } from '#imports'
import { AppActionExtern, type AppAction } from '@/domain/externs/AppAction'
import { InjectAdapter } from '@/service/adapter/runtimeMessage'
import { defineProxy } from 'comctx'
const [, injectAppAction] = defineProxy(() => ({}) as AppAction, {
namespace: browser.runtime.id
})
const appAction = injectAppAction(new InjectAdapter())
export const AppActionImpl = AppActionExtern.impl(appAction)

View File

@@ -1,13 +1,10 @@
import { NotificationExtern } from '@/domain/externs/Notification'
import type { TextMessage } from '@/domain/ChatRoom'
import { EVENT } from '@/constants/event'
import { messenger } from '@/messenger'
import { NotificationExtern, type Notification } from '@/domain/externs/Notification'
class Notification {
async push(message: TextMessage) {
await messenger.sendMessage(EVENT.NOTIFICATION_PUSH, message)
return message.id
}
}
import { InjectAdapter } from '@/service/adapter/runtimeMessage'
import { defineProxy } from 'comctx'
export const NotificationImpl = NotificationExtern.impl(new Notification())
const [, injectNotification] = defineProxy(() => ({}) as Notification)
const notification = injectNotification(new InjectAdapter())
export const NotificationImpl = NotificationExtern.impl(notification)

View File

@@ -1,11 +0,0 @@
import type { EVENT } from '@/constants/event'
import { defineExtensionMessaging } from '@webext-core/messaging'
import type { TextMessage } from '@/domain/ChatRoom'
interface ProtocolMap {
[EVENT.OPTIONS_PAGE_OPEN]: () => void
[EVENT.NOTIFICATION_PUSH]: (message: TextMessage) => void
[EVENT.NOTIFICATION_CLEAR]: (id: string) => void
}
export const messenger = defineExtensionMessaging<ProtocolMap>()

View File

@@ -0,0 +1,8 @@
import type { AppAction as AppActionExternType } from '@/domain/externs/AppAction'
import { browser } from '#imports'
export class AppAction implements AppActionExternType {
async openOptionsPage() {
await browser.runtime.openOptionsPage()
}
}

View File

@@ -0,0 +1,49 @@
import type { Notification as NotificationExternType } from '@/domain/externs/Notification'
import type { TextMessage } from '@/domain/ChatRoom'
import { browser } from '#imports'
import type { MessageTab } from '@/service/adapter/runtimeMessage'
export class Notification implements NotificationExternType {
historyNotificationTabs = new Map<string, MessageTab>()
constructor() {
browser.notifications.onButtonClicked.addListener(async (id) => {
const formTab = this.historyNotificationTabs.get(id)
if (formTab?.id) {
try {
const tab = await browser.tabs.get(formTab.id)
browser.tabs.update(tab.id!, { active: true, highlighted: true })
browser.windows.update(tab.windowId!, { focused: true })
} catch {
browser.tabs.create({ url: formTab.url })
}
}
})
browser.notifications.onClicked.addListener(async (id) => {
const fromTab = this.historyNotificationTabs.get(id)
if (fromTab?.id) {
try {
const tab = await browser.tabs.get(fromTab.id)
browser.tabs.update(tab.id!, { active: true })
} catch {
browser.tabs.create({ url: fromTab.url })
}
}
})
browser.notifications.onClosed.addListener(async (id) => {
this.historyNotificationTabs.delete(id)
})
}
async push(message: TextMessage & { meta?: { tab?: MessageTab } }) {
const tab = message.meta?.tab
console.log(tab)
const id = await browser.notifications.create({
type: 'basic',
iconUrl: message.userAvatar,
title: message.username,
message: message.body,
contextMessage: tab?.url
})
tab && this.historyNotificationTabs.set(id, tab)
}
}

View File

@@ -0,0 +1,46 @@
import { browser } from '#imports'
import type { Browser } from '#imports'
import type { Adapter, Message, SendMessage, OnMessage } from 'comctx'
export interface MessageTab {
id?: number
url?: string
}
export interface MessageMeta {
tab?: MessageTab
}
export class ProvideAdapter implements Adapter<MessageMeta> {
sendMessage: SendMessage<MessageMeta> = async (message) => {
const tabs = await browser.tabs.query({ url: message.meta.tab?.url })
tabs.map((tab) => browser.tabs.sendMessage(tab.id!, message))
browser.runtime.sendMessage(message)
}
onMessage: OnMessage<MessageMeta> = (callback) => {
const handler = (message: Partial<Message<MessageMeta>>, sender: Browser.runtime.MessageSender) => {
callback({ ...message, meta: { tab: { id: sender.tab?.id, url: sender.tab?.url } } })
}
browser.runtime.onMessage.addListener(handler)
return () => browser.runtime.onMessage.removeListener(handler)
}
}
export class InjectAdapter implements Adapter<MessageMeta> {
sendMessage: SendMessage<MessageMeta> = (message) => {
browser.runtime.sendMessage(browser.runtime.id, {
...message,
meta: { tab: { url: document.location.href } }
})
}
onMessage: OnMessage<MessageMeta> = (callback) => {
const handler = (message?: Partial<Message<MessageMeta>>) => {
callback(message)
}
browser.runtime.onMessage.addListener(handler)
return () => browser.runtime.onMessage.removeListener(handler)
}
}