Skip to content

Conversation

@cyfung1031
Copy link
Collaborator

@cyfung1031 cyfung1031 commented Nov 14, 2025

概述 Descriptions

依存: #949

变更内容 Changes

截图 Screenshots


测试代码(一):

修改后:GM.listValues() 能在冲突中取得最新,而且不会因本地缓存与valueUpdate冲突而造成次序不一
useAsync 改为 false 的话就能看 GM_xxxx 的结果 )

// ==UserScript==
// @name         测试 GM.getValue 能否取得最新值
// @namespace    yourname.scripts
// @version      0.1.0
// @description  把当前网页URL保存到存储列表中
// @author       You
// @match        *://*/*
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_deleteValue
// @grant        GM_listValues
// @grant        GM.getValue
// @grant        GM.setValue
// @grant        GM.deleteValue
// @grant        GM.listValues
// @grant        GM_addValueChangeListener
// @grant        GM_removeValueChangeListener
// ==/UserScript==

(function () {
    'use strict';
    let useAsync = true;
    const rid = `${(Math.floor(Math.random() * 5000) + 4000).toString(36)}`;

    const trigger = async () => {
        console.log(`[${rid}]`,"trigger in " + location.href, "current time = " + Date.now());
        useAsync ? action2() : action1();
    }
    let k = 0;
    const action1 = async () => {
        ++k;
        GM_setValue(`${"list_"}${rid}${k}`, Date.now());
        console.log(`[${rid}]`, GM_listValues());
    };

    const action2 = async () => {
        ++k;
        await GM.setValue(`${"list_"}${rid}${k}`, Date.now());
        console.log(`[${rid}]`, await GM.listValues());
    };

    const wins = [];
    let tc0;
    window.addEventListener("message", (e) => {
        if (e.data && typeof e.data === "object" && e.data?.test_call_id && e.data?.type === "response_tc") {
            const tc1 = e.data.test_call_id;
            if (tc1 === tc0) {
                trigger();
            }
        }
    });
    if (location.search.startsWith("?test_call=") && top !== window) {
        const usp = new URLSearchParams(location.search);
        const tc = usp.get("test_call");
        if (tc) {
            tc0 = tc;
            window.top.postMessage({ type: "done_iframe_tc", test_call_id: tc }, "*");
        }
    } else if (window.location.href.includes("example.com") && top === window) {
        const test_call_id = `tc${Date.now()}_${Math.random()}`;
        tc0 = test_call_id;
        let q = 1;
        let ec = 0;
        const doFunc = async (elements) => {
            console.log(`[${rid}]`,"---- ADD SOME INFO... ------");
            await (useAsync ? action2() : action1());
            await (useAsync ? action2() : action1());
            await (useAsync ? action2() : action1());
            console.log(`[${rid}]`,"---------------------------");
            setTimeout(() => {
                console.log(`[${rid}]`,"do trigger");
                for (const iframe of elements) {
                    try {
                        iframe.contentWindow.postMessage({ type: "response_tc", test_call_id },
                            "*"
                        )
                    } catch (e) {
                        // ignored
                    }
                }
                window.postMessage({ type: "response_tc", test_call_id }, "*");

                setTimeout(async () => {
                     console.log(`[${rid}]`,"final list", await GM.listValues());
                }, 1500)
            }, 1500);
        }
        window.addEventListener("message", (e) => {
            if (e.data && typeof e.data === "object" && e.data?.test_call_id && e.data?.type === "done_iframe_tc") {
                const tc = e.data.test_call_id;
                if (tc === test_call_id) {
                    const elements = document.querySelectorAll("iframe.tt0011");
                    // wins.push([e.source, e.origin]);
                    wins.push(1);
                    if (wins.length === elements.length && elements.length === ec && q) {
                        q = 0;
                        doFunc(elements);
                    }
                }
            }
        });
        
        const makeIframe = () => {
            const elm = document.body.appendChild(document.createElement("iframe"));
            elm.classList.add("tt0011");
            return elm;
        }

        ec = 3;
        setTimeout(()=>{

            makeIframe().src = `https://example.com/?test_call=${test_call_id}`;
        }, 1800);

        setTimeout(()=>{

            makeIframe().src = `https://example.com/?test_call=${test_call_id}`;
        }, 2400);

        setTimeout(() => {
            makeIframe().src = `https://example.com/?test_call=${test_call_id}`;
        }, 3200);
    }
})();

测试代码(二):

(有GM_lock 做时间控制)修改后 GM.getValue 的列表新增没问题

// ==UserScript==
// @name         Example Script for GM_lock
// @namespace    yourname.scripts
// @version      0.1
// @description  把当前网页URL保存到存储列表中
// @author       You
// @match        *://*/*
// @grant        GM.getValue
// @grant        GM.setValue
// @grant        GM.setValues
// @grant        GM.deleteValue
// @grant        GM.deleteValues
// @grant        GM.listValues
// @grant        GM_addValueChangeListener
// @grant        GM_removeValueChangeListener
// @require      https://update.greasyfork.org/scripts/554436/1692608/GM_lock.js
// @noframes
// ==/UserScript==

/* global GM_lock */
(function () {
  'use strict';
  GM_lock("lock_urls", async () => {
    console.log("开始", Date.now(), performance.now());
    // 等一下这个页面SC的缓存更新
    // await new Promise(resolve => setTimeout(resolve, 50));
    // 从存储中读取已有的列表
    let list = await GM.getValue('list', []); // 设置默认值为空数组
    // 如果当前URL不在列表中,就添加进去
    console.log("初始列表:", list.slice());
    if (!list.includes(location.href)) {
      list.push(location.href);
      await GM.setValue('list', list);
      console.log('✅ 已保存此页面到列表:', location.href);
    } else {
      console.log('ℹ️ 当前页面已在列表中');
    }
    // 可选:在控制台查看当前列表
    console.log('当前列表:', list.slice());
    // 等一下其他页面SC的缓存更新
    // await new Promise(resolve => setTimeout(resolve, 50));
    console.log("结束", Date.now(), performance.now());
  });
})();

@cyfung1031 cyfung1031 changed the title 異步 getValue/getValues/listValues 相关修改 異步 getValue/getValues/listValues 相关修改 & 修正 deleteValue/deleteValues 无法执行问题 Nov 14, 2025
@CodFrm
Copy link
Member

CodFrm commented Nov 15, 2025

怎么都到这个分支去了 develop/raw-message

@cyfung1031
Copy link
Collaborator Author

怎么都到这个分支去了 develop/raw-message

因为两边的 commit 互相影响
但处理的内容不一样,写在同一PR又太多又乱

@CodFrm CodFrm changed the base branch from develop/raw-message to release/v1.3 November 15, 2025 14:10
@cyfung1031 cyfung1031 changed the title 異步 getValue/getValues/listValues 相关修改 & 修正 deleteValue/deleteValues 无法执行问题 異步 getValue/getValues/listValues 相关修改 Nov 16, 2025
@cyfung1031 cyfung1031 force-pushed the develop-values-api-8 branch 3 times, most recently from b6d77de to 31a4165 Compare November 17, 2025 23:52
@cyfung1031 cyfung1031 changed the title 異步 getValue/getValues/listValues 相关修改 [v1.3] 異步 getValue/getValues/listValues 相关修改 Nov 22, 2025
@CodFrm CodFrm requested a review from Copilot November 28, 2025 07:04
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

这个 PR 实现了异步 getValue/getValues/listValues 相关的重要改进,主要解决了在多标签页并发场景下值读取的一致性问题。通过引入 waitForFreshValueState 机制和批处理架构,确保在读取值之前能够获得最新的状态。

主要变更:

  • 新增 waitForFreshValueState 方法,确保在读取前获取最新的 value 状态
  • 重构 setValues 方法为批处理架构,使用任务队列和 setValuesByStorageName 进行批量处理
  • 修改 GM.getValue/GM.listValues/GM.getValues 以在读取前等待最新状态

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/app/service/service_worker/value.ts 核心变更:新增 waitForFreshValueStatesetValuesByStorageName,重构 value 更新逻辑为批处理模式,引入 ValueUpdateTaskInfo 任务队列
src/app/service/service_worker/value.test.ts 更新测试以反映新的批处理行为,调整期望值以匹配新的数据结构,添加 flush() 调用处理异步逻辑
src/app/service/service_worker/runtime.ts 更新 valueUpdate 订阅以使用新的 TScriptValueUpdate 类型,添加脚本状态重新验证逻辑
src/app/service/service_worker/permission_verify.ts 更新泛型约束从 TT extends Array<any> 以匹配 API 参数类型
src/app/service/service_worker/gm_api/gm_api.ts 新增 internalApiWaitForFreshValueState API 方法,修正权限链接配置
src/app/service/sandbox/runtime.ts 更新 valueUpdate 方法以处理新的 ValueUpdateSendData 数据结构
src/app/service/queue.ts 修改 TScriptValueUpdate 类型定义,从包含 Script 对象改为包含 uuidstatusisEarlyStart 字段
src/app/service/content/types.ts ValueUpdateDataEncoded 中添加 updatetime 字段,新增 ValueUpdateSendData 类型
src/app/service/content/script_executor.ts 更新 valueUpdate 方法签名以适配新的数据结构
src/app/service/content/inject.ts 更新类型引用从 ValueUpdateDataEncodedValueUpdateSendData
src/app/service/content/gm_api/gm_api.ts 实现 waitForFreshValueState 静态方法,更新 GM.getValue/GM.listValues/GM.getValues 以调用该方法,重构 valueUpdate 处理多个更新事件,引入 extValueStoreCopyreadFreshes 机制
src/app/service/content/gm_api/gm_api.test.ts 添加 valueDaoUpdatetimeFix 辅助函数,更新测试以处理新的 waitForFreshValueState 行为,修正正则表达式以匹配计数器格式
src/app/service/content/exec_script.ts 更新 valueUpdate 方法签名以传递 storageNameuuid 和数据列表

@CodFrm CodFrm added the P1 🔥 重要但是不紧急的内容 label Dec 9, 2025
Comment on lines +195 to +197
const hold = deferred();
// 避免立即 emit
stackAsyncTask("valueUpdateEventListenerEmit", () => hold.promise);
Copy link
Member

Choose a reason for hiding this comment

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

为什么不直接包裹,而且整个流程都是同步的,用stackAsyncTask的意义是什么

Copy link
Member

Choose a reason for hiding this comment

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

如果是想 完全处理完毕后再 emitToListener,我觉得不如直接把这个逻辑放在最后

Copy link
Collaborator Author

@cyfung1031 cyfung1031 Jan 2, 2026

Choose a reason for hiding this comment

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

emitToListener 跟 valueChangePromiseMap 使用同一条 task 队列
valueUpdate 开始时,放了 stackAsyncTask("valueUpdateEventListenerEmit", () => hold.promise);
这让 emitToListener 先不开始处理
valueUpdate 完结后, hold.resolve(); 执行, emitToListener里的才开始处理
< 注意如果有其他 valueUpdate 的 valueChangeListener 未跑好,這輪的 valueChangeListener 也不會立即開始 >

用其他方法也可以
例如做一个 tasklist.
emitToListener 时把 this, key, oldValue, value, remote, sender.tabId 储存成一个 entry 放在 tasklist
最后 forloop 执行
这样写比较长,但效果一样
(這個只能考慮同一輪的valueChangeListener執行)

可是这个不保证 valueUpdate 二次 三次执行的顺序问题
而且 valueChangeListener 的執行會拖慢 valueUpdate 的完結
(考慮 userscript作者會利用 valueChangeListener 加一堆長時間執行的東西)
如果 valueUpdate 不等 valueChangeListener 的執行,第二次的第2個valueChangeListener 可能會較第一次的第3個 valueChangeListener 先執行

统一用 valueUpdateEventListenerEmit 的话,任何 valueUpdate 的 valueChangeListener 都要乖乖排队
这确保次序必为一致,不会插队。而且 valueUpdate 的執行不會受到 valueChangeListener 的執行而被拖慢
因為有 stackAsyncTask("valueUpdateEventListenerEmit", () => hold.promise);,所以第一個 valueChangeListener 的操作不會被立即執行,而是等 promise resolve 後的下一個 microTask

當要考慮上面所講的所有考慮,用現在這個做法是最簡單,否則寫長3倍 4倍的代碼還一堆 bug

@CodFrm
Copy link
Member

CodFrm commented Jan 2, 2026

唉,真的需要这么复杂的处理吗?有点看不下去

waitForFreshValueState 既然都发消息给 SW 了,为什么不直接返回要get的值

@cyfung1031
Copy link
Collaborator Author

waitForFreshValueState 既然都发消息给 SW 了,为什么不直接返回要get的值

waitForFreshValueState 是用来等待最新值经valueUpdate 反映
"GM.getValue" "GM.listValues" "GM.getValues" 还是用本身的 GM_getValue, GM_listValues, GM_getValues 取本地的来做

如果另外处理的话,日后维护可能会令 "GM.getValue" 和 GM_getValue 的结果不一致
(SW处理跟本地处理)

waitForFreshValueState 不返回值让设计灵活简单一点

@CodFrm
Copy link
Member

CodFrm commented Jan 2, 2026

waitForFreshValueState 是用来等待最新值经valueUpdate 反映 "GM.getValue" "GM.listValues" "GM.getValues" 还是用本身的 GM_getValue, GM_listValues, GM_getValues 取本地的来做

如果另外处理的话,日后维护可能会令 "GM.getValue" 和 GM_getValue 的结果不一致 (SW处理跟本地处理)

只可能是值更新了,但是消息还没发出去到达content进行处理,才会不一致,而且这个不一致是合理的,因为GM.getValue/GM_getValue就是应该要取到最新的值,只是GM_getValue因为是同步的,可能取的并不是最新

waitForFreshValueState 不返回值让设计灵活简单一点

太弯弯绕绕了

@cyfung1031
Copy link
Collaborator Author

waitForFreshValueState 是用来等待最新值经valueUpdate 反映 "GM.getValue" "GM.listValues" "GM.getValues" 还是用本身的 GM_getValue, GM_listValues, GM_getValues 取本地的来做
如果另外处理的话,日后维护可能会令 "GM.getValue" 和 GM_getValue 的结果不一致 (SW处理跟本地处理)

只可能是值更新了,但是消息还没发出去到达content进行处理,才会不一致,而且这个不一致是合理的,因为GM.getValue/GM_getValue就是应该要取到最新的值,只是GM_getValue因为是同步的,可能取的并不是最新

waitForFreshValueState 不返回值让设计灵活简单一点

太弯弯绕绕了

waitForFreshValueState 是等SW给最新的值
如果直接取SW的值,会导致那个值跟 GM_addValueChangeListener 最后取得的值有机会不一致
现在只是等SW给东西,确认了是页面当前的取得的「最新」后,就用 GM_getValue 拿数值
(实际上在 waitForFreshValueState resolve后,其他tab 也可能在呼叫SW改数值,只是还没广播到页面)
值也会跟页面收到 GM_addValueChangeListener的资讯一致 ( valueA -> valueB -> valueC <- GM_getValue 也是 valueC)

@cyfung1031
Copy link
Collaborator Author

cyfung1031 commented Jan 2, 2026

waitForFreshValueState 的设计是,发一个 internalApiWaitForFreshValueState 给SW,在SW那边插一个 task 到 SW valueUpdate 队列

假设 waitForFreshValueState 的发生时间是 t=4.1s,
SW插进去肯定是包含 t=4.1s 之前所有 valueUpdate
因此 internalApiWaitForFreshValueState 返回时,就可以确定要等到那些 valueUpdate 接收好,本地的数据才会是 >= t=4.1s
此时 SW的数据可能已经是又变动了 (t=4.6s, 4.8s)
但 waitForFreshValueState resolve 后已经保证是 t=4.1s 以前的变动通通 apply 了,所以可以取 GM_getValue 值

这是相对于 setValue 要等 SW 设置后才 Promise结束
getValue 是等 頁面 读取后才 Promise开始

@cyfung1031
Copy link
Collaborator Author

cyfung1031 commented Jan 2, 2026

waitForFreshValueState 不返回值让设计灵活简单一点

太弯弯绕绕了

可以移除 waitForFreshValueState
改成一个通用的方式跟SW取最新值
getFreshValues(keys: []) -> listValues
getFreshValues(keys: ["key1"]) -> getValue("key1")
getFreshValues(keys: ["key1", "key2"]) -> getValues(["key1", "key2"])

(PR其他修改不变)
(没通过 页面valueUpdate接收处理, 不保证 GM.getValue 的东西跟 valueChangeListener 的触发和页面数据会否一致 。 不清楚隐藏了什么样的 bug. 由于跟 valueChange 处理不一致,可能影响到现时脚本)

@cyfung1031
Copy link
Collaborator Author

waitForFreshValueState 不返回值让设计灵活简单一点

太弯弯绕绕了

可以移除 waitForFreshValueState 改成一个通用的方式跟SW取最新值 getFreshValues(keys: []) -> listValues getFreshValues(keys: ["key1"]) -> getValue("key1") getFreshValues(keys: ["key1", "key2"]) -> getValues(["key1", "key2"])

(PR其他修改不变) (没通过 页面valueUpdate接收处理, 不保证 GM.getValue 的东西跟 valueChangeListener 的触发和页面数据会否一致 。 不清楚隐藏了什么样的 bug. 由于跟 valueChange 处理不一致,可能影响到现时脚本)

我觉得加 waitForFreshValueState 只是一个辅助。不必改变 GM.getValue 的本质。
即使改动了,事实上 .getValue 和 .setValue 还是避免不了多个执行是有冲突的问题
如果为了一个少少修改,影响了本来的运作,就是本末倒置了

这个修改不像 GM.setValue 那么大影响(之前 GM.setValue 会因为页面关掉而没有实际呼叫 SW 改值,影响大)
也找不到脚本去说明这个PR的重要性
所以可以长期放置,不合并也可以

@cyfung1031 cyfung1031 marked this pull request as draft January 2, 2026 07:28
@cyfung1031
Copy link
Collaborator Author

我把争议大的部份先拿走,移至 #1125

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

Labels

P1 🔥 重要但是不紧急的内容

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants