diff --git a/packages/message/common.ts b/packages/message/common.ts index 799a8526d..1e2a3c60d 100644 --- a/packages/message/common.ts +++ b/packages/message/common.ts @@ -68,6 +68,95 @@ export function getEventFlag(messageFlag: string, onReady: (eventFlag: string) = pageDispatchCustomEvent(messageFlag, { action: "requestEventFlag" }); } +/** + * 在同一个页面中,通过自定义事件「协商」出一个唯一可用的 EventFlag + * + * 设计目的: + * - 页面中可能同时存在多个实例 + * - 需要确保最终只有一个 EventFlag 被选中并使用 + * + * 协商思路(基于同步事件机制): + * 1. 先广播一次【不带 EventFlag 的询问事件】 + * 2. 所有实例都会收到该事件,并根据收到的内容做判断: + * - 如果收到【已带 EventFlag 的事件】 + * → 说明已有实例成功声明旗标,直接采用该值 + * → 如果不是自己期望的旗标,立刻退出协商 + * - 如果收到【不带 EventFlag 的事件】 + * → 视为一次“空回应” + * → 在可接受次数内,主动声明自己的 preferredFlag + * 3. 若空回应次数超过上限仍未成功,则放弃协商 + * + * 注意事项: + * - dispatchEvent 是同步执行的 + * - 实例也会收到自己发出的事件 + * - 只有一个实例时,通常立即采用 preferredFlag + * - 多实例并存时,先成功拦截并声明的实例胜出 + */ +export function getSyncFlag(channelKey: string, preferredFlag: string, maxEmptyResponses: number = 3) { + /** 协商所使用的事件名称 */ + const eventName = `${channelKey}_syncFlag`; + + /** 最终确认并采用的 EventFlag */ + let finalFlag = ""; + + /** 已收到的“空事件”次数(不带 EventFlag) */ + let emptyEventCount = 0; + + /** + * 处理协商事件的核心监听函数 + */ + const fnHandler: EventListener = (event) => { + if (!(event instanceof CustomEvent)) return; + if (event.defaultPrevented) return; + + const receivedFlag = event.detail?.EventFlag; + + // ───────────── 情况一:收到已声明 EventFlag 的事件 ───────────── + if (receivedFlag) { + // 只在尚未确定最终结果时处理 + if (!finalFlag) { + finalFlag = receivedFlag; + + // 若旗标不是自己期望的,说明其他实例已胜出 + if (receivedFlag !== preferredFlag) { + pageRemoveEventListener(eventName, fnHandler); + } + } + return; + } + + // ───────────── 情况二:收到不带 EventFlag 的空事件 ───────────── + emptyEventCount++; + + if (emptyEventCount <= maxEmptyResponses) { + // 在允许范围内,主动声明自己的旗标 + pageDispatchCustomEvent(eventName, { + EventFlag: preferredFlag, + }); + + // 阻止事件继续传播,避免被其他实例抢先处理 + event.preventDefault(); + event.stopImmediatePropagation(); + event.stopPropagation(); + } else { + // 超过最大尝试次数,放弃协商 + pageRemoveEventListener(eventName, fnHandler); + } + }; + + // 开始监听协商事件 + pageAddEventListener(eventName, fnHandler); + + // 发送第一次询问事件(不带 EventFlag) + pageDispatchCustomEvent(eventName, {}); + + if (!finalFlag) { + throw new Error("Unexpected Error in syncFlag"); + } + + return finalFlag; +} + export const createMouseEvent = process.env.VI_TESTING === "true" ? (type: string, eventInitDict?: MouseEventInit | undefined): MouseEvent => { diff --git a/src/content.ts b/src/content.ts index 41e7e6b4d..8b22756c9 100644 --- a/src/content.ts +++ b/src/content.ts @@ -4,29 +4,32 @@ import { CustomEventMessage } from "@Packages/message/custom_event_message"; import { Server } from "@Packages/message/server"; import { ScriptExecutor } from "./app/service/content/script_executor"; import type { Message } from "@Packages/message/types"; -import { getEventFlag } from "@Packages/message/common"; +import { getEventFlag, getSyncFlag } from "@Packages/message/common"; import { ScriptRuntime } from "./app/service/content/script_runtime"; import { ScriptEnvTag } from "@Packages/message/consts"; import { isContent } from "./app/service/content/gm_api/gm_api"; +import { randomMessageFlag } from "./pkg/utils/utils"; const messageFlag = process.env.SC_RANDOM_KEY!; +const syncFlag = getSyncFlag(messageFlag, randomMessageFlag(), 3); +const scriptEnvTag = isContent ? ScriptEnvTag.content : ScriptEnvTag.inject; -getEventFlag(messageFlag, (eventFlag: string) => { - const scriptEnvTag = isContent ? ScriptEnvTag.content : ScriptEnvTag.inject; +const msg: Message = new CustomEventMessage(`${syncFlag}${scriptEnvTag}`, false); - const msg: Message = new CustomEventMessage(`${eventFlag}${scriptEnvTag}`, false); +// 初始化日志组件 +const logger = new LoggerCore({ + writer: new MessageWriter(msg, "scripting/logger"), + consoleLevel: process.env.NODE_ENV === "development" ? "debug" : "none", // 只让日志在scripting环境中打印 + labels: { env: "content", href: window.location.href }, +}); +logger.logger().debug("content start"); - // 初始化日志组件 - const logger = new LoggerCore({ - writer: new MessageWriter(msg, "scripting/logger"), - consoleLevel: process.env.NODE_ENV === "development" ? "debug" : "none", // 只让日志在scripting环境中打印 - labels: { env: "content", href: window.location.href }, - }); +const server = new Server("content", msg); +const scriptExecutor = new ScriptExecutor(msg); - logger.logger().debug("content start"); +getEventFlag(messageFlag, (_eventFlag: string) => { + logger.logger().debug("content getEventFlag"); - const server = new Server("content", msg); - const scriptExecutor = new ScriptExecutor(msg); const runtime = new ScriptRuntime(scriptEnvTag, server, msg, scriptExecutor, messageFlag); runtime.init(); }); diff --git a/src/inject.ts b/src/inject.ts index 17dc18e50..ffb9d4116 100644 --- a/src/inject.ts +++ b/src/inject.ts @@ -4,29 +4,32 @@ import { CustomEventMessage } from "@Packages/message/custom_event_message"; import { Server } from "@Packages/message/server"; import { ScriptExecutor } from "./app/service/content/script_executor"; import type { Message } from "@Packages/message/types"; -import { getEventFlag } from "@Packages/message/common"; +import { getEventFlag, getSyncFlag } from "@Packages/message/common"; import { ScriptRuntime } from "./app/service/content/script_runtime"; import { ScriptEnvTag } from "@Packages/message/consts"; import { isContent } from "./app/service/content/gm_api/gm_api"; +import { randomMessageFlag } from "./pkg/utils/utils"; const messageFlag = process.env.SC_RANDOM_KEY!; +const syncFlag = getSyncFlag(messageFlag, randomMessageFlag(), 3); +const scriptEnvTag = isContent ? ScriptEnvTag.content : ScriptEnvTag.inject; -getEventFlag(messageFlag, (eventFlag: string) => { - const scriptEnvTag = isContent ? ScriptEnvTag.content : ScriptEnvTag.inject; +const msg: Message = new CustomEventMessage(`${syncFlag}${scriptEnvTag}`, false); - const msg: Message = new CustomEventMessage(`${eventFlag}${scriptEnvTag}`, false); +// 初始化日志组件 +const logger = new LoggerCore({ + writer: new MessageWriter(msg, "scripting/logger"), + consoleLevel: process.env.NODE_ENV === "development" ? "debug" : "none", // 只让日志在scripting环境中打印 + labels: { env: "inject", href: window.location.href }, +}); +logger.logger().debug("inject start"); - // 初始化日志组件 - const logger = new LoggerCore({ - writer: new MessageWriter(msg, "scripting/logger"), - consoleLevel: process.env.NODE_ENV === "development" ? "debug" : "none", // 只让日志在scripting环境中打印 - labels: { env: "inject", href: window.location.href }, - }); +const server = new Server("inject", msg); +const scriptExecutor = new ScriptExecutor(msg); - logger.logger().debug("inject start"); +getEventFlag(messageFlag, (_eventFlag: string) => { + logger.logger().debug("inject getEventFlag"); - const server = new Server("inject", msg); - const scriptExecutor = new ScriptExecutor(msg); const runtime = new ScriptRuntime(scriptEnvTag, server, msg, scriptExecutor, messageFlag); runtime.init(); diff --git a/src/scripting.ts b/src/scripting.ts index 9ff6d0bb1..d9130029b 100644 --- a/src/scripting.ts +++ b/src/scripting.ts @@ -6,30 +6,35 @@ import { CustomEventMessage } from "@Packages/message/custom_event_message"; import { ScriptEnvTag } from "@Packages/message/consts"; import { Server } from "@Packages/message/server"; import ScriptingRuntime from "./app/service/content/scripting"; -import { negotiateEventFlag } from "@Packages/message/common"; +import { getSyncFlag, negotiateEventFlag } from "@Packages/message/common"; +import { randomMessageFlag } from "./pkg/utils/utils"; const messageFlag = process.env.SC_RANDOM_KEY!; +const syncFlag = getSyncFlag(messageFlag, randomMessageFlag(), 3); -// 将初始化流程完成后,将EventFlag通知到其他环境 -negotiateEventFlag(messageFlag, 2, (eventFlag) => { - // 建立与service_worker页面的连接 - const extMsgComm: Message = new ExtensionMessage(false); - // 初始化日志组件 - const logger = new LoggerCore({ - writer: new MessageWriter(extMsgComm, "serviceWorker/logger"), - labels: { env: "scripting" }, - }); +// 建立与service_worker页面的连接 +const extMsgComm: Message = new ExtensionMessage(false); +// 初始化日志组件 +const logger = new LoggerCore({ + writer: new MessageWriter(extMsgComm, "serviceWorker/logger"), + labels: { env: "scripting" }, +}); + +logger.logger().debug("scripting start"); - logger.logger().debug("scripting start"); +const contentMsg = new CustomEventMessage(`${syncFlag}${ScriptEnvTag.content}`, true); +const injectMsg = new CustomEventMessage(`${syncFlag}${ScriptEnvTag.inject}`, true); - const contentMsg = new CustomEventMessage(`${eventFlag}${ScriptEnvTag.content}`, true); - const injectMsg = new CustomEventMessage(`${eventFlag}${ScriptEnvTag.inject}`, true); +const server = new Server("scripting", [contentMsg, injectMsg]); - const server = new Server("scripting", [contentMsg, injectMsg]); +// Opera中没有chrome.runtime.onConnect,并且content也不需要chrome.runtime.onConnect +// 所以不需要处理连接,设置为false +const extServer = new Server("scripting", extMsgComm, false); + +// 将初始化流程完成后,将EventFlag通知到其他环境 +negotiateEventFlag(messageFlag, 2, (_eventFlag) => { + logger.logger().debug("scripting negotiateEventFlag"); - // Opera中没有chrome.runtime.onConnect,并且content也不需要chrome.runtime.onConnect - // 所以不需要处理连接,设置为false - const extServer = new Server("scripting", extMsgComm, false); // scriptExecutor的消息接口 // 初始化运行环境 const runtime = new ScriptingRuntime(extServer, server, extMsgComm, contentMsg, injectMsg);