Skip to content

Conversation

@CodFrm
Copy link
Member

@CodFrm CodFrm commented Jan 28, 2026

概述 Descriptions

@cyfung1031 更简洁的flag协商方式,尽量不变动原来的结构,删除多余逻辑

你看看大概有没有什么问题,没有就先合并到你的分支上,再做调整

感觉这个 it.concurrent,机器配置不够的时候,总是会超时,并发也容易出问题,后续还是用普通的好了

变更内容 Changes

截图 Screenshots

@CodFrm CodFrm changed the base branch from develop/messaging-performance-boost2 to release/v1.3 January 28, 2026 10:16
@CodFrm CodFrm changed the base branch from release/v1.3 to develop/messaging-performance-boost2 January 28, 2026 10:16
@cyfung1031 cyfung1031 self-requested a review January 28, 2026 12:26
@cyfung1031
Copy link
Collaborator

感觉这个 it.concurrent,机器配置不够的时候,总是会超时,并发也容易出问题,后续还是用普通的好了

是重構的方式有問題嗎
之前都沒試過這樣
你這個重構了才有

src/content.ts Outdated
Comment on lines 14 to 16
const EventFlag = randomMessageFlag();

// 判断当前是否运行在 USER_SCRIPT 环境 (content环境)
const isContent = typeof chrome.runtime?.sendMessage === "function";
const scriptEnvTag = isContent ? ScriptEnvTag.content : ScriptEnvTag.inject;
negotiateEventFlag(MessageFlag, EventFlag);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

怎麼這個跟 inject.ts 不一樣了?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

由content生成随机flag,scripting和inject获取

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

這樣不對
不是 1067 的設計

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1067
scripting 提供 EventFlag
inject.ts 和 content.ts 接收

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

因为我还是考虑chrome不要scripting,另外由谁提供EventFlag这个无所谓的

@cyfung1031
Copy link
Collaborator

感觉这个 it.concurrent,机器配置不够的时候,总是会超时,并发也容易出问题,后续还是用普通的好了

是重構的方式有問題嗎 之前都沒試過這樣 你這個重構了才有

我本地跑 pnpm test 也跑不過
你到底改了什麼鬼。感覺壞掉了

@cyfung1031 cyfung1031 marked this pull request as draft January 28, 2026 12:50
@CodFrm
Copy link
Member Author

CodFrm commented Jan 28, 2026

感觉这个 it.concurrent,机器配置不够的时候,总是会超时,并发也容易出问题,后续还是用普通的好了

是重構的方式有問題嗎 之前都沒試過這樣 你這個重構了才有

我本地跑 pnpm test 也跑不過

你到底改了什麼鬼。感覺壞掉了

有坏掉的,但是有的是因为超时时间过短,又并发,导致处理不过来,加超时时间可以解决,坏掉的我明天看

@cyfung1031
Copy link
Collaborator

感觉这个 it.concurrent,机器配置不够的时候,总是会超时,并发也容易出问题,后续还是用普通的好了

是重構的方式有問題嗎 之前都沒試過這樣 你這個重構了才有

我本地跑 pnpm test 也跑不過
你到底改了什麼鬼。感覺壞掉了

有坏掉的,但是有的是因为超时时间过短,又并发,导致处理不过来,加超时时间可以解决,坏掉的我明天看

我觉得不是超时问题。就是没成功呼叫
测试内容没改过
从这个 5003511 开始就已经单元测试跑不动

@cyfung1031
Copy link
Collaborator

cyfung1031 commented Jan 28, 2026

this.pageMessaging.onReady!(() => {
...
})

这个是需要的

考虑到 early-start 什么的会比握手更早呼叫 GM API
握手成功后才跑 GM API

因为 GM API 都要跟 service_worker 溝通
本身就是异步
因此可以这样做

Comment on lines 50 to 70
export function getEventFlag(messageFlag: string): string {
let eventFlag = "";
const EventFlagListener: EventListener = (ev) => {
if (!(ev instanceof CustomEvent)) return;
if (ev.detail?.action != "broadcastEventFlag") return;
eventFlag = ev.detail.EventFlag;
pageRemoveEventListener(messageFlag, EventFlagListener);
// 告知对方已收到 EventFlag
pageDispatchCustomEvent(messageFlag, { action: "receivedEventFlag" });
};

pageAddEventListener(messageFlag, EventFlagListener);

// 基于同步机制,判断是否已经收到 EventFlag
// 如果没有收到,则主动请求一次
if (!eventFlag) {
pageDispatchCustomEvent(messageFlag, { action: "requestEventFlag" });
}

return eventFlag;
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@CodFrm
getEventFlag 好像是取得 inject <-> content 的 flag? 这不是1067的设计原意

1067 的设计是 inject 和 content 都是同级的
scripting才是上级
所以 inject 和 content 所有代码,除了 window.external 注入
都是一致的

const EventFlag = randomMessageFlag();
negotiateEventFlag(MessageFlag, EventFlag);

不会在 content.js 搞


另外,根据执行次序,不一定马上会有 EventFlag
所以 1067 的 CustomEventMessage 里的 flag 一开始就不是固定
会有 onReady 等行为

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

确实目前 inject 和 content 除了 window.external 就没什么其它区别了,但是考虑到chrome并不需要scripting,以content为主也方便后续调整。对于协商flag这一块谁为主次无所谓的

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

這樣搞兩個版本會好混亂
直接統一成scripting不好嗎
不然日後bug report什麼的又會分兩種處理
我是想整個處理變得通用
不需要考慮chrome或firefox

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

你说得也有道理,两套不同的架构也加大维护成本

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

关于引入 scripting 和 使用 chrome.storage 作为广播 都挺纠结的,前者是因为Firefox妥协,后者是 方法的职责和性能问题,valueUpdate并不完全是广播

src/scripting.ts Outdated
// scripting <-> inject/content 的双向消息桥
const scriptExecutorMsgTxIT = new CustomEventMessage(scriptExecutorMsgIT, true); // 双向:scripting <-> inject
const scriptExecutorMsgTxCT = new CustomEventMessage(scriptExecutorMsgCT, true); // 双向:scripting <-> content
const EventFlag = getEventFlag(MessageFlag);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1067
scripting 提供 EventFlag
inject.ts 和 content.ts 接收

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

由谁提供无所谓的,只要保持3个环境一致就行了

@cyfung1031
Copy link
Collaborator

cyfung1031 commented Jan 28, 2026

scripting.js 是主
content.js 和 inject.js 是副
content.js 和 inject.js 的代码应几乎完全一样
在通讯设计,通讯都要发给 SW 再callback传回,或接收SW的
这都是 scripting.js 做

假如完全没有 @inject-into content
执行上可以完全不需要 content.js
但不会影响 scripting.jsinject.js 的对话
当然现在还不是这样设计。没 @inject-into content 的话 content.js 还是会执行一下初始化,使 readyCount = 2

但整个概念要搞清楚主和客
这是 1067 其中一个重要的改变

(以往 content.js 做的都搬到 scripting.js 做)
(不需要 scripting.js <-> content.js <-> inject.js 这做法)
(只有 scripting.js <-> content.js 和 scripting.js <-> inject.js )
(scripting.js content.js inject.js 三者的环境都不一样。我们不用理会受限制的 content.js 环境。只理会插件的scripting环境和页面的page环境)
(content.js 和 inject.js 一樣是跑代碼用的)


另一个改变是
这个 EventFlag 不一定是一开始就有
你不要去想一开始就有
你永远不知道 scripting, inject, content 的执行次序
应该说三者总有第一个执行
如第一个执行的不是产生 EventFlag 的,你的这个设计就会完全崩溃
(1067 的设计不是做三方会谈。不理会谁是第一。当然三方会谈让第一个产生 EventFlag 会是理想做法,但实际操作复杂)

early-start 什么的是直接跑代码
就算没有EventFlag也都要顺利跑代码

在通讯设计,通讯都要发给 SW 再callback传回,或接收SW的

这个本身就是异步,不影响

如果是同步的话,基于 inject/content 没有另一个脚本的执行,要考虑报错的可能性
(不要有 content 没scripting 也能跑部份API等复杂想法)
这影响的是同步API,例如 addElement 等
当然实际上 scripting 是最先跑的。
所以 1067 的设计,即使 early-start 也好,实际上跑同步API都不会出错


好吧剛發現Brave的次序是 content->inject->scripting
沒測試Firefox

那就做三方會談的EventFlag吧

@cyfung1031
Copy link
Collaborator

cyfung1031 commented Jan 28, 2026

6d510a6

三方会谈版 EventFlag

// 避免页面载入后改动全域物件导致消息传递失败
const MouseEventClone = MouseEvent;
const CustomEventClone = CustomEvent;
const performanceClone = performance;

// 避免页面载入后改动 EventTarget.prototype 的方法导致消息传递失败
const pageDispatchEvent = performanceClone.dispatchEvent.bind(performanceClone);
const pageAddEventListener = performanceClone.addEventListener.bind(performanceClone);
const pageRemoveEventListener = performanceClone.removeEventListener.bind(performanceClone);

const detailClone = typeof cloneInto === "function" ? cloneInto : null;

const pageDispatchCustomEvent = (eventType, detail) => {
  if (detailClone && detail) {
    detail = detailClone(detail, performanceClone);
  }
  const ev = new CustomEventClone(eventType, {
    detail,
    cancelable: true,
  });
  return pageDispatchEvent(ev);
};

// flag 协商
function negotiateEventFlag(messageFlag, firstEventFlag, responsedCountMax = 3) {
  const tag = `${messageFlag}_negotiate`;
  let eventFlag = "";

  let responsedCount = 0;

  const EventFlagRequestHandler = (ev) => {
    if (!(ev instanceof CustomEvent)) return;

    switch (ev.detail && ev.detail.action) {
      case "responseEventFlag":
        if (ev.defaultPrevented) return;
        if (eventFlag || !ev.detail || !ev.detail.EventFlag) return;

        eventFlag = ev.detail.EventFlag;
        if (eventFlag !== firstEventFlag) {
          pageRemoveEventListener(tag, EventFlagRequestHandler);
        }
        break;

      case "requestEventFlag":
        if (ev.defaultPrevented) return;

        responsedCount++;
        if (responsedCount <= responsedCountMax) {
          pageDispatchCustomEvent(tag, {
            action: "responseEventFlag",
            EventFlag: firstEventFlag,
          });
          ev.preventDefault();
          ev.stopImmediatePropagation();
          ev.stopPropagation();
        } else {
          pageRemoveEventListener(tag, EventFlagRequestHandler);
        }
        break;
    }
  };

  pageAddEventListener(tag, EventFlagRequestHandler);
  pageDispatchCustomEvent(tag, { action: "requestEventFlag" });

  if (!eventFlag) {
    console.error("negotiateEventFlag failed");
  }

  return eventFlag;
}

const mf = "amjmajsj1";
console.log(negotiateEventFlag(mf, "a123", 3)); // a123
console.log(negotiateEventFlag(mf, "a456", 3)); // a123
console.log(negotiateEventFlag(mf, "a689", 3)); // a123
console.log(negotiateEventFlag(mf, "a477", 3)); // a477
console.log(negotiateEventFlag(mf, "a488", 3)); // a477
console.log(negotiateEventFlag(mf, "a499", 3)); // a477
console.log(negotiateEventFlag(mf, "a511", 3)); // a511
console.log(negotiateEventFlag(mf, "a522", 3)); // a511
console.log(negotiateEventFlag(mf, "a533", 3)); // a511

@CodFrm
Copy link
Member Author

CodFrm commented Jan 29, 2026

this.pageMessaging.onReady!(() => { ... })

这个是需要的

考虑到 early-start 什么的会比握手更早呼叫 GM API 握手成功后才跑 GM API

因为 GM API 都要跟 service_worker 溝通 本身就是异步 因此可以这样做

并不需要,early-start的检测执行,在协商完成之后了,flag协商都没完成,是不会跑脚本的

@CodFrm
Copy link
Member Author

CodFrm commented Jan 29, 2026

啊,是我搞错了,我的代码有问题,我再看看,我以为我的 negotiateEventFlag/getEventFlag 和 scripting/content/inject 顺序无关的,确实需要个 onReady

如果 scripting, inject, content 顺序发生变化的话,会有你说的问题 我再看看

@CodFrm
Copy link
Member Author

CodFrm commented Jan 29, 2026

我又改动了一下,negotiateEventFlag 放在了scripting,不过现在放在任何环境/顺序都无所谓了 (debug信息故意保留,确定后再删除)

@cyfung1031
Copy link
Collaborator

cyfung1031 commented Jan 29, 2026

我又改动了一下,negotiateEventFlag 放在了scripting,不过现在放在任何环境/顺序都无所谓了 (debug信息故意保留,确定后再删除)

你用我這個不就好了嗎??

6d510a6

這個不用理誰先誰後
總之三個都一致

@CodFrm
Copy link
Member Author

CodFrm commented Jan 29, 2026

我又改动了一下,negotiateEventFlag 放在了scripting,不过现在放在任何环境/顺序都无所谓了 (debug信息故意保留,确定后再删除)

你用我這個不就好了嗎??

6d510a6

没有 onReady 的逻辑呀,会出现你说的问题,而且不好理解

@CodFrm
Copy link
Member Author

CodFrm commented Jan 29, 2026

关于单元测试,我把 --isolate=false 选项删除后,就可以通过了,看起来是有什么修改影响了。。。。

@cyfung1031
Copy link
Collaborator

关于单元测试,我把 --isolate=false 选项删除后,就可以通过了,看起来是有什么修改影响了。。。。

還是過不了呀
我還是覺得是這個PR改動的問題

@cyfung1031
Copy link
Collaborator

我又改动了一下,negotiateEventFlag 放在了scripting,不过现在放在任何环境/顺序都无所谓了 (debug信息故意保留,确定后再删除)

你用我這個不就好了嗎??
6d510a6

没有 onReady 的逻辑呀,会出现你说的问题,而且不好理解

唉呀...
1067 原本的设计
checkEarlyStartScript 是不需要 onReady 的
也就是early-start 能直接跑

@cyfung1031
Copy link
Collaborator

我又改动了一下,negotiateEventFlag 放在了scripting,不过现在放在任何环境/顺序都无所谓了 (debug信息故意保留,确定后再删除)

你用我這個不就好了嗎??
6d510a6

没有 onReady 的逻辑呀,会出现你说的问题,而且不好理解

如果你只考虑基本执行的根本不需要EventFlag呀
early-start 的话不等待其他脚本的载入,同步的话预期出错。异步才会等待
所以1067 是弹性处理。不是一刀切一定要等两边载入,或者不等其他载入

EventFlag本身不用做等待。先執行先取得。就是 6d510a6 的做法

src/content.ts Outdated
import { ScriptRuntime } from "./app/service/content/script_runtime";
import { ScriptEnvTag } from "@Packages/message/consts";

const MessageFlag = process.env.SC_RANDOM_KEY || "scriptcat-default-flag";
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

這個fallback是多餘的

@cyfung1031
Copy link
Collaborator

cyfung1031 commented Jan 29, 2026

我只能說 1067 是花了一整個月想出來的東西
你這樣重構會限制了大多數的設計
unit test 失敗也是證明了重構不行

包括 early-start 不受握手程序的限制
SW異步完美對應EventFlag等


老實說,一次性的代碼,也要做一個Class去 new 來 new 去,不建議
我不清楚為什麼你堅持這種寫法
class 的問題是, this. 這個問題。要 bind 來 bind去
還有 這些class都是 public
導致class本身的名字,class裡面的method, 全部都無法minified
打包後的代碼會較大

現代的JavaScript根本不是玩OOP
JavaScript不需要OOP


你看回我1067的代碼,可以很容易理解哪個部份是同步,哪個部份是異步

scripting

import LoggerCore from "./app/logger/core";
import MessageWriter from "./app/logger/message_writer";
import { CustomEventMessage, createPageMessaging } from "@Packages/message/custom_event_message";
import { pageAddEventListener, pageDispatchCustomEvent } from "@Packages/message/common";
import { ScriptEnvTag, ScriptEnvType } from "@Packages/message/consts";
import { uuidv5 } from "./pkg/utils/uuid";
import { randomMessageFlag, makeBlobURL } from "@App/pkg/utils/utils";
import { ExtensionMessage } from "@Packages/message/extension_message";
import type { Message, MessageSend } from "@Packages/message/types";
import { Server, forwardMessage } from "@Packages/message/server";
import { RuntimeClient } from "@App/app/service/service_worker/client";
import type { Logger } from "@App/app/repo/logger";
import { MessageDelivery } from "./message-delivery";

const MessageFlag = uuidv5(`${performance.timeOrigin}`, process.env.SC_RANDOM_KEY);
const uuids = new Map<string, ScriptEnvType>();
const senderToExt: Message = new ExtensionMessage(false);
const scriptExecutorMsgIT = createPageMessaging("");
const scriptExecutorMsgCT = createPageMessaging("");
const scriptExecutorMsgTxIT = new CustomEventMessage(scriptExecutorMsgIT, true); // 双向:scripting <-> inject
const scriptExecutorMsgTxCT = new CustomEventMessage(scriptExecutorMsgCT, true); // 双向:scripting <-> content
const loggerCore = new LoggerCore({
  writer: new MessageWriter(senderToExt, "serviceWorker/logger"),
  labels: { env: "scripting" },
});
const scriptingMessaging = createPageMessaging(""); // 对 inject / content 的 client 发出消息
const messageDeliveryToPage = new MessageDelivery();
const client = new RuntimeClient(senderToExt);
loggerCore.logger().debug("scripting start");

const requireScriptingToken = (): string => {
  // ...
};

const setupDeliveryChannel = () => {
  // ...
};

// ================================
// Server 构建与 service_worker 转发
// ================================

type GmApiPayload = { api: string; params: any; uuid: string };

const handleRuntimeGmApi = (
  senderToInject: CustomEventMessage,
  senderToContent: CustomEventMessage,
  data: GmApiPayload
) => {
  // ...
};

const prepareServer = (
  server: Server,
  senderToExt: MessageSend,
  senderToInject: CustomEventMessage,
  senderToContent: CustomEventMessage
) => {
  // ...
};

// ================================
// 握手:MessageFlag 与 injectFlagEvt 协商
// ================================

const onMessageFlagReceived = (MessageFlag: string) => {
  const executorEnvReadyKey = uuidv5("scriptcat-executor-ready", MessageFlag);

  // 由 scripting 随机生成,用于 scripting <-> inject/content 的消息通道 token
  const injectFlagEvt = randomMessageFlag();

  // readyFlag 位运算:inject=1,content=2,凑齐 3 表示都 ready. ready 后设为 4 避免再触发
  let readyFlag = 0;

  const finalizeWhenReady = () => {
  // ...
  };

  // 接收 inject/content 的 ready 回执
  pageAddEventListener(`${injectFlagEvt}`, (ev) => {
  // ...
  });

  // 向 inject/content 广播 injectFlagEvt(让它们知道后续用哪个 token 通信)
  const submitTarget = () => {
  // ...
  };

  // 处理“scripting 早于 content/inject 执行”的场景:
  // content/inject 会先发一个 executorEnvReadyKey(detail 为空)来探测 scripting 是否在
  pageAddEventListener(executorEnvReadyKey, (ev) => {
  // ...
  });

  // 处理“scripting 晚于 content/inject 执行”的场景:
  // scripting 启动后主动广播一次 executorEnvReadyKey,content/inject 立刻能收到 injectFlagEvt
  submitTarget();
};

chrome.storage.local.onChanged.addListener((changes) => {
  // ...
});

chrome.runtime.onMessage.addListener((message, _sender) => {
  // ...
});

// ================================
// 启动流程
// ================================

onMessageFlagReceived(MessageFlag);

client.pageLoad().then((o) => {
  // ...
});
  • 你可以直接看到 onMessageFlagReceived 是同步跑的

inject.js

import LoggerCore from "./app/logger/core";
import MessageWriter from "./app/logger/message_writer";
import { CustomEventMessage, createPageMessaging } from "@Packages/message/custom_event_message";
import { pageAddEventListener, pageDispatchCustomEvent, pageDispatchEvent } from "@Packages/message/common";
import { ScriptEnvTag } from "@Packages/message/consts";
import { uuidv5 } from "./pkg/utils/uuid";
import { initEnvInfo, ScriptExecutor } from "./app/service/content/script_executor";
import type { ValueUpdateDataEncoded } from "./app/service/content/types";
import type { TClientPageLoadInfo } from "./app/repo/scripts";
import type { Message } from "@Packages/message/types";
import { sendMessage } from "@Packages/message/client";
import { ExternalWhitelist } from "@App/app/const";

const MessageFlag = uuidv5(`${performance.timeOrigin}`, process.env.SC_RANDOM_KEY);

// ================================
// 常量与全局状态
// ================================

const isContent = typeof chrome.runtime?.sendMessage === "function";
const scriptEnvTag = isContent ? ScriptEnvTag.content : ScriptEnvTag.inject;
const executorEnvReadyKey = uuidv5("scriptcat-executor-ready", MessageFlag);
const scriptingMessaging = createPageMessaging(""); // injectFlagEvt
const pageMessaging = createPageMessaging(""); // `${injectFlagEvt}_${scriptEnvTag}`
const msg = new CustomEventMessage(pageMessaging, false);
const logger = new LoggerCore({
  writer: new MessageWriter(msg, "scripting/logger"),
  consoleLevel: "none",
  labels: { env: "inject", href: window.location.href },
});
const scriptExecutor = new ScriptExecutor(msg);
let bindScriptingDeliveryOnce: (() => void) | null = null;

const requireScriptingToken = (): string => {
  // ...
};

const resetMessagingTokens = () => {
  // ...
};

const setMessagingTokens = (injectFlagEvt: string) => {
  // ...
};

const acknowledgeScriptingReady = (injectFlagEvt: string) => {
  // ...
};

// ================================
// 对外接口:external 注入
// ================================

// 判断当前 hostname 是否命中白名单(含子域名)
function isExternalWhitelisted(hostname: string) {
  // ...
}

// 生成暴露给页面的 Scriptcat 外部接口
function createScriptcatExpose(pageMsg: Message) {
  // ...
}

// 尝试写入 external,失败则忽略
function safeSetExternal<T extends object>(external: any, key: string, value: T) {
  // ...
}

// 当 TM 与 SC 同时存在时的兼容处理:TM 未安装脚本时回退查询 SC
function patchTampermonkeyIsInstalled(external: any, scriptExpose: App.ExternalScriptCat) {
  // ...
}

// inject 环境 pageLoad 后执行:按白名单对页面注入 external 接口
function onInjectPageLoaded(pageMsg: Message) {
  // ...
}

// ================================
// 消息分发处理
// ================================

// 处理 scripting -> inject 的消息
const handleDeliveryMessage = (tag: string, value: any) => {
  // ...
};

// ================================
// 页面通信绑定与握手
// ================================

// 监听 scripting 发来的 delivery 消息
const bindScriptingDeliveryChannel = () => {
  // ...
};

// 建立 scripting <-> inject 的握手流程
const setupHandshake = () => {
  // 准备一次性绑定函数
  bindScriptingDeliveryOnce = () => {
  // ...
  };

  // 等待 scripting 注入完成并发送 injectFlagEvt (仅调用一次)
  pageAddEventListener(executorEnvReadyKey, (ev) => {
  // ...
  });
};

// ================================
// 启动流程
// ================================

// 检查 early-start 脚本
scriptExecutor.checkEarlyStartScript(scriptEnvTag, initEnvInfo);

// 建立握手与通信绑定
setupHandshake();

// 主动触发 ready 事件,请求 scripting 建立连接
pageDispatchEvent(new CustomEvent(executorEnvReadyKey));
  • 你可以直接看到 scriptExecutor.checkEarlyStartScript 是同步跑的

@cyfung1031
Copy link
Collaborator

cyfung1031 commented Jan 29, 2026

关于单元测试,我把 --isolate=false 选项删除后,就可以通过了,看起来是有什么修改影响了。。。。

单元测试的问题解决了

没有 onReady 的逻辑呀,会出现你说的问题,而且不好理解

关于 这个 1162 的PR,令 early-start 不能直接跑的 "onReady", 你解决一下吧

還有 server.on 不能放在異步裡

我還是推以下寫法

/**
 * 在同一个页面中,通过自定义事件「协商」出一个唯一可用的 EventFlag
 *
 * 设计目的:
 * - 页面中可能同时存在多个实例
 * - 需要确保最终只有一个 EventFlag 被选中并使用
 *
 * 协商思路(基于同步事件机制):
 * 1. 先广播一次【不带 EventFlag 的询问事件】
 * 2. 所有实例都会收到该事件,并根据收到的内容做判断:
 *    - 如果收到【已带 EventFlag 的事件】
 *        → 说明已有实例成功声明旗标,直接采用该值
 *        → 如果不是自己期望的旗标,立刻退出协商
 *    - 如果收到【不带 EventFlag 的事件】
 *        → 视为一次“空回应”
 *        → 在可接受次数内,主动声明自己的 preferredFlag
 * 3. 若空回应次数超过上限仍未成功,则放弃协商
 *
 * 注意事项:
 * - dispatchEvent 是同步执行的
 * - 实例也会收到自己发出的事件
 * - 只有一个实例时,通常立即采用 preferredFlag
 * - 多实例并存时,先成功拦截并声明的实例胜出
 */
export function negotiateEventFlag(channelKey: string, preferredFlag: string, maxEmptyResponses: number = 3) {
  /** 协商所使用的事件名称 */
  const eventName = `${channelKey}_negotiate`;

  /** 最终确认并采用的 EventFlag */
  let finalFlag = "";

  /** 已收到的“空事件”次数(不带 EventFlag) */
  let emptyEventCount = 0;

  /**
   * 处理协商事件的核心监听函数
   */
  const onNegotiationEvent: 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, onNegotiationEvent);
        }
      }
      return;
    }

    // ───────────── 情况二:收到不带 EventFlag 的空事件 ─────────────
    emptyEventCount++;

    if (emptyEventCount <= maxEmptyResponses) {
      // 在允许范围内,主动声明自己的旗标
      pageDispatchCustomEvent(eventName, {
        EventFlag: preferredFlag,
      });

      // 阻止事件继续传播,避免被其他实例抢先处理
      event.preventDefault();
      event.stopImmediatePropagation();
      event.stopPropagation();
    } else {
      // 超过最大尝试次数,放弃协商
      pageRemoveEventListener(eventName, onNegotiationEvent);
    }
  };

  // 开始监听协商事件
  pageAddEventListener(eventName, onNegotiationEvent);

  // 发送第一次询问事件(不带 EventFlag)
  pageDispatchCustomEvent(eventName, {});

  if (!finalFlag) {
    throw new Error("negotiateEventFlag: 未能成功协商出 EventFlag");
  }

  return finalFlag;
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants