From 34c560775dff8393f88fc73bcc109ad8a87cb35d Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Fri, 2 Jan 2026 18:40:25 +0900 Subject: [PATCH] =?UTF-8?q?GM=20value=20=E4=BB=A3=E7=A0=81=E7=9B=B8?= =?UTF-8?q?=E5=85=B3=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/repo/scripts.ts | 4 +- src/app/service/content/content.ts | 2 +- src/app/service/content/exec_script.ts | 21 +- src/app/service/content/gm_api/gm_api.test.ts | 131 ++++++---- src/app/service/content/gm_api/gm_api.ts | 101 +++++--- src/app/service/content/inject.ts | 4 +- src/app/service/content/script_executor.ts | 21 +- src/app/service/content/types.ts | 15 +- src/app/service/queue.ts | 4 +- src/app/service/sandbox/runtime.ts | 53 ++-- .../service/service_worker/gm_api/gm_api.ts | 2 +- .../service_worker/permission_verify.ts | 7 +- src/app/service/service_worker/runtime.ts | 21 +- src/app/service/service_worker/value.test.ts | 199 ++++++++++----- src/app/service/service_worker/value.ts | 238 ++++++++++++------ 15 files changed, 527 insertions(+), 296 deletions(-) diff --git a/src/app/repo/scripts.ts b/src/app/repo/scripts.ts index d4ba61491..6fcc2b86f 100644 --- a/src/app/repo/scripts.ts +++ b/src/app/repo/scripts.ts @@ -93,10 +93,12 @@ export interface ScriptSite { export type ScriptAndCode = Script & ScriptCode; +export type ValueStore = { [key: string]: any }; + // 脚本运行时的资源,包含已经编译好的脚本与脚本需要的资源 export interface ScriptRunResource extends Script { code: string; // 原始代码 - value: { [key: string]: any }; + value: ValueStore; flag: string; resource: { [key: string]: { base64?: string } & Omit }; // 资源列表,包含脚本需要的资源 metadata: SCMetadata; // 经自定义覆盖的 Metadata diff --git a/src/app/service/content/content.ts b/src/app/service/content/content.ts index ec39006c4..6b785097f 100644 --- a/src/app/service/content/content.ts +++ b/src/app/service/content/content.ts @@ -77,7 +77,7 @@ export default class ContentRuntime { let parentNode: EventTarget | undefined; // 判断是不是content脚本发过来的 let msg: CustomEventMessage; - if (this.contentScriptSet.has(data.uuid) || this.scriptExecutor.execMap.has(data.uuid)) { + if (this.contentScriptSet.has(data.uuid) || this.scriptExecutor.execScriptMap.has(data.uuid)) { msg = this.scriptExecutorMsg; } else { msg = this.senderToInject; diff --git a/src/app/service/content/exec_script.ts b/src/app/service/content/exec_script.ts index 9d54bf883..30fb9668d 100644 --- a/src/app/service/content/exec_script.ts +++ b/src/app/service/content/exec_script.ts @@ -7,7 +7,8 @@ import type { Message } from "@Packages/message/types"; import type { ValueUpdateDataEncoded } from "./types"; import { evaluateGMInfo } from "./gm_api/gm_info"; import type { IGM_Base } from "./gm_api/gm_api"; -import type { TScriptInfo } from "@App/app/repo/scripts"; +import type { ScriptRunResource, TScriptInfo } from "@App/app/repo/scripts"; +import { getStorageName } from "@App/pkg/utils/utils"; // 执行脚本,控制脚本执行与停止 export default class ExecScript { @@ -64,8 +65,22 @@ export default class ExecScript { this.sandboxContext?.emitEvent(event, eventId, data); } - valueUpdate(data: ValueUpdateDataEncoded) { - this.sandboxContext?.valueUpdate(data); + valueUpdate(storageName: string, uuid: string, responses: ValueUpdateDataEncoded[]) { + const scriptRes = this.scriptRes; + if (scriptRes.uuid === uuid || getStorageName(scriptRes) === storageName) { + const context = this.sandboxContext; + if (context) { + const contextScriptRes = context.scriptRes as ScriptRunResource | null | undefined; + if (contextScriptRes) { + if (uuid === contextScriptRes.uuid || storageName === getStorageName(contextScriptRes)) { + contextScriptRes.value = context.extValueStoreCopy || contextScriptRes.value; + const valueStore = contextScriptRes.value; + context.valueStoreUpdate(valueStore, responses); + context.extValueStoreCopy = { ...valueStore }; + } + } + } + } } execContext: any; diff --git a/src/app/service/content/gm_api/gm_api.test.ts b/src/app/service/content/gm_api/gm_api.test.ts index 28c46ee12..6abca840c 100644 --- a/src/app/service/content/gm_api/gm_api.test.ts +++ b/src/app/service/content/gm_api/gm_api.test.ts @@ -6,6 +6,7 @@ import { compileScript, compileScriptCode } from "../utils"; import type { Message } from "@Packages/message/types"; import { encodeRValue } from "@App/pkg/utils/message_value"; import { v4 as uuidv4 } from "uuid"; +import { getStorageName } from "@App/pkg/utils/utils"; const nilFn: ScriptFunc = () => {}; const scriptRes = { @@ -123,10 +124,14 @@ describe.concurrent("GM Api", () => { script.value = { test: "ok" }; script.metadata.grant = ["GM.getValue"]; script.code = `return GM.getValue("test").then(v=>v+"!");`; - // @ts-ignore - const exec = new ExecScript(script, undefined, undefined, nilFn, envInfo); + const mockSendMessage = vi.fn().mockResolvedValue({ code: 0 }); + const mockMessage = { + sendMessage: mockSendMessage, + } as unknown as Message; + const exec = new ExecScript(script, "content", mockMessage, nilFn, envInfo); exec.scriptFunc = compileScript(compileScriptCode(script)); - const ret = await exec.exec(); + const retPromise = exec.exec(); + const ret = await retPromise; expect(ret).toEqual("ok!"); }); @@ -135,10 +140,14 @@ describe.concurrent("GM Api", () => { script.value = { test1: "23", test2: "45", test3: "67" }; script.metadata.grant = ["GM_listValues"]; script.code = `return GM_listValues().join("-");`; - // @ts-ignore - const exec = new ExecScript(script, undefined, undefined, nilFn, envInfo); + const mockSendMessage = vi.fn().mockResolvedValue({ code: 0 }); + const mockMessage = { + sendMessage: mockSendMessage, + } as unknown as Message; + const exec = new ExecScript(script, "content", mockMessage, nilFn, envInfo); exec.scriptFunc = compileScript(compileScriptCode(script)); - const ret = await exec.exec(); + const retPromise = exec.exec(); + const ret = await retPromise; expect(ret).toEqual("test1-test2-test3"); }); @@ -151,11 +160,15 @@ describe.concurrent("GM Api", () => { script.value.test1 = "40"; script.metadata.grant = ["GM_listValues"]; script.code = `return GM_listValues().join("-");`; - // @ts-ignore - const exec = new ExecScript(script, undefined, undefined, nilFn, envInfo); + const mockSendMessage = vi.fn().mockResolvedValue({ code: 0 }); + const mockMessage = { + sendMessage: mockSendMessage, + } as unknown as Message; + const exec = new ExecScript(script, "content", mockMessage, nilFn, envInfo); exec.scriptFunc = compileScript(compileScriptCode(script)); - const ret = await exec.exec(); - expect(ret).toEqual("test5-test2-test3-test1"); // TM也沒有sort + const retPromise = exec.exec(); + const ret = await retPromise; + expect(ret).toEqual("test5-test2-test3-test1"); // TM也没有sort }); it.concurrent("GM.listValues", async () => { @@ -163,10 +176,14 @@ describe.concurrent("GM Api", () => { script.value = { test1: "23", test2: "45", test3: "67" }; script.metadata.grant = ["GM.listValues"]; script.code = `return GM.listValues().then(v=>v.join("-"));`; - // @ts-ignore - const exec = new ExecScript(script, undefined, undefined, nilFn, envInfo); + const mockSendMessage = vi.fn().mockResolvedValue({ code: 0 }); + const mockMessage = { + sendMessage: mockSendMessage, + } as unknown as Message; + const exec = new ExecScript(script, "content", mockMessage, nilFn, envInfo); exec.scriptFunc = compileScript(compileScriptCode(script)); - const ret = await exec.exec(); + const retPromise = exec.exec(); + const ret = await retPromise; expect(ret).toEqual("test1-test2-test3"); }); @@ -179,11 +196,15 @@ describe.concurrent("GM Api", () => { script.value.test1 = "40"; script.metadata.grant = ["GM.listValues"]; script.code = `return GM.listValues().then(v=>v.join("-"));`; - // @ts-ignore - const exec = new ExecScript(script, undefined, undefined, nilFn, envInfo); + const mockSendMessage = vi.fn().mockResolvedValue({ code: 0 }); + const mockMessage = { + sendMessage: mockSendMessage, + } as unknown as Message; + const exec = new ExecScript(script, "content", mockMessage, nilFn, envInfo); exec.scriptFunc = compileScript(compileScriptCode(script)); - const ret = await exec.exec(); - expect(ret).toEqual("test5-test2-test3-test1"); // TM也沒有sort + const retPromise = exec.exec(); + const ret = await retPromise; + expect(ret).toEqual("test5-test2-test3-test1"); // TM也没有sort }); it.concurrent("GM_getValues", async () => { @@ -212,10 +233,14 @@ describe.concurrent("GM Api", () => { script.value = { test1: "23", test2: 45, test3: "67" }; script.metadata.grant = ["GM.getValues"]; script.code = `return GM.getValues(["test2", "test3", "test1"]).then(v=>v);`; - // @ts-ignore - const exec = new ExecScript(script, undefined, undefined, nilFn, envInfo); + const mockSendMessage = vi.fn().mockResolvedValue({ code: 0 }); + const mockMessage = { + sendMessage: mockSendMessage, + } as unknown as Message; + const exec = new ExecScript(script, "content", mockMessage, nilFn, envInfo); exec.scriptFunc = compileScript(compileScriptCode(script)); - const ret = await exec.exec(); + const retPromise = exec.exec(); + const ret = await retPromise; expect(ret.test1).toEqual("23"); expect(ret.test2).toEqual(45); expect(ret.test3).toEqual("67"); @@ -499,7 +524,7 @@ describe.concurrent("GM_value", () => { api: "GM_setValues", params: [ // event id - expect.stringMatching(/^.+::\d$/), + expect.stringMatching(/^.+::\d+$/), // the object payload keyValuePairs1, ], @@ -523,7 +548,7 @@ describe.concurrent("GM_value", () => { api: "GM_setValues", params: [ // event id - expect.stringMatching(/^.+::\d$/), + expect.stringMatching(/^.+::\d+$/), // the object payload keyValuePairs2, ], @@ -573,7 +598,7 @@ describe.concurrent("GM_value", () => { api: "GM_setValues", params: [ // event id - expect.stringMatching(/^.+::\d$/), + expect.stringMatching(/^.+::\d+$/), // the object payload keyValuePairs1, ], @@ -592,7 +617,7 @@ describe.concurrent("GM_value", () => { api: "GM_setValue", params: [ // event id - expect.stringMatching(/^.+::\d$/), + expect.stringMatching(/^.+::\d+$/), // the string payload "b", ], @@ -643,7 +668,7 @@ describe.concurrent("GM_value", () => { api: "GM_setValues", params: [ // event id - expect.stringMatching(/^.+::\d$/), + expect.stringMatching(/^.+::\d+$/), // the object payload keyValuePairs1, ], @@ -667,7 +692,7 @@ describe.concurrent("GM_value", () => { api: "GM_setValues", params: [ // event id - expect.stringMatching(/^.+::\d$/), + expect.stringMatching(/^.+::\d+$/), // the string payload keyValuePairs2, ], @@ -702,14 +727,16 @@ describe.concurrent("GM_value", () => { const retPromise = exec.exec(); expect(mockSendMessage).toHaveBeenCalledTimes(1); // 模拟值变化 - exec.valueUpdate({ - id: "id-1", - entries: [["param1", encodeRValue(123), encodeRValue(undefined)]], - uuid: script.uuid, - storageName: script.uuid, - sender: { runFlag: exec.sandboxContext!.runFlag, tabId: -2 }, - valueUpdated: true, - }); + exec.valueUpdate(getStorageName(script), script.uuid, [ + { + id: "id-1", + valueChanges: [["param1", encodeRValue(123), encodeRValue(undefined)]], + uuid: script.uuid, + storageName: script.uuid, + sender: { runFlag: exec.sandboxContext!.runFlag, tabId: -2 }, + updatetime: Date.now(), + }, + ]); const ret = await retPromise; expect(ret).toEqual({ name: "param1", oldValue: undefined, newValue: 123, remote: false }); }); @@ -737,14 +764,16 @@ describe.concurrent("GM_value", () => { const retPromise = exec.exec(); expect(mockSendMessage).toHaveBeenCalledTimes(1); // 模拟值变化 - exec.valueUpdate({ - id: "id-2", - entries: [["param2", encodeRValue(456), encodeRValue(undefined)]], - uuid: script.uuid, - storageName: "testStorage", - sender: { runFlag: "user", tabId: -2 }, - valueUpdated: true, - }); + exec.valueUpdate(getStorageName(script), script.uuid, [ + { + id: "id-2", + valueChanges: [["param2", encodeRValue(456), encodeRValue(undefined)]], + uuid: script.uuid, + storageName: "testStorage", + sender: { runFlag: "user", tabId: -2 }, + updatetime: Date.now(), + }, + ]); const ret2 = await retPromise; expect(ret2).toEqual({ name: "param2", oldValue: undefined, newValue: 456, remote: true }); }); @@ -772,14 +801,16 @@ describe.concurrent("GM_value", () => { expect(id).toBeTypeOf("string"); expect(id.length).greaterThan(0); // 触发valueUpdate - exec.valueUpdate({ - id: id, - entries: [["a", encodeRValue(123), encodeRValue(undefined)]], - uuid: script.uuid, - storageName: script.uuid, - sender: { runFlag: exec.sandboxContext!.runFlag, tabId: -2 }, - valueUpdated: true, - }); + exec.valueUpdate(getStorageName(script), script.uuid, [ + { + id: id, + valueChanges: [["a", encodeRValue(123), encodeRValue(undefined)]], + uuid: script.uuid, + storageName: script.uuid, + sender: { runFlag: exec.sandboxContext!.runFlag, tabId: -2 }, + updatetime: Date.now(), + }, + ]); const ret = await retPromise; expect(ret).toEqual(123); diff --git a/src/app/service/content/gm_api/gm_api.ts b/src/app/service/content/gm_api/gm_api.ts index b8d0f0ce7..1d762a813 100644 --- a/src/app/service/content/gm_api/gm_api.ts +++ b/src/app/service/content/gm_api/gm_api.ts @@ -14,10 +14,9 @@ import { base64ToBlob, randNum, randomMessageFlag, strToBase64 } from "@App/pkg/ import LoggerCore from "@App/app/logger/core"; import EventEmitter from "eventemitter3"; import GMContext from "./gm_context"; -import { type ScriptRunResource } from "@App/app/repo/scripts"; +import type { ValueStore, ScriptRunResource } from "@App/app/repo/scripts"; import type { ValueUpdateDataEncoded } from "../types"; import { connect, sendMessage } from "@Packages/message/client"; -import { getStorageName } from "@App/pkg/utils/utils"; import { ListenerManager } from "../listener_manager"; import { decodeRValue, encodeRValue, type REncoded } from "@App/pkg/utils/message_value"; import { type TGMKeyValue } from "@App/app/repo/value"; @@ -28,7 +27,7 @@ import { convObjectToURL, GM_xmlhttpRequest, toBlobURL, urlToDocumentInContentPa export interface IGM_Base { sendMessage(api: string, params: any[]): Promise; connect(api: string, params: any[]): Promise; - valueUpdate(data: ValueUpdateDataEncoded): void; + valueStoreUpdate(valueStore: ValueStore, responses: ValueUpdateDataEncoded[]): void; emitEvent(event: string, eventId: string, data: any): void; } @@ -43,7 +42,19 @@ let valChangeCounterId = 0; let valChangeRandomId = `${randNum(8e11, 2e12).toString(36)}`; -const valueChangePromiseMap = new Map(); +type PromiseResolve = ((...args: any[]) => any) | null | undefined; + +const valueChangePromiseMap = new Map(); + +const generateValChangeId = () => { + if (valChangeCounterId > 1e8) { + // 防止 valChangeCounterId 过大导致无法正常工作 + valChangeCounterId = 0; + valChangeRandomId = `${randNum(8e11, 2e12).toString(36)}`; + } + const id = `${valChangeRandomId}::${++valChangeCounterId}`; + return id; +}; const execEnvInit = (execEnv: GMApi) => { if (!execEnv.contentEnvKey) { @@ -54,6 +65,21 @@ const execEnvInit = (execEnv: GMApi) => { } }; +const emitToListener = ( + a: GM_Base, + key: string, + oldValue: any, + value: any, + remote: boolean, + tabId: number | undefined // 注: tabId 的提供不在标准 GM API 的定义 +) => { + // 放在下一个 microTask; 在 valueUpdate 完成后才放行。避免卡住 valueUpdate 线程 + Promise.resolve().then(() => { + // 不等待结果 + a.valueChangeListener?.execute(key, oldValue, value, remote, tabId); + }); +}; + // GM_Base 定义内部用变量和函数。均使用@protected // 暂不考虑 Object.getOwnPropertyNames(GM_Base.prototype) 和 ts-morph 脚本生成 class GM_Base implements IGM_Base { @@ -73,7 +99,7 @@ class GM_Base implements IGM_Base { // Extension Context 无效时释放 valueChangeListener @GMContext.protected() - protected valueChangeListener?: ListenerManager; + public valueChangeListener?: ListenerManager; // Extension Context 无效时释放 EE @GMContext.protected() @@ -110,6 +136,9 @@ class GM_Base implements IGM_Base { @GMContext.protected() public setInvalidContext!: () => void; + @GMContext.protected() + public extValueStoreCopy: Record | undefined; // 使每个tab的valueChange次序保持一致 + // 单次回调使用 @GMContext.protected() public async sendMessage(api: string, params: any[]) { @@ -149,12 +178,10 @@ class GM_Base implements IGM_Base { } @GMContext.protected() - public valueUpdate(data: ValueUpdateDataEncoded) { - if (!this.scriptRes || !this.valueChangeListener) return; - const scriptRes = this.scriptRes; - const { id, uuid, entries, storageName, sender, valueUpdated } = data; - if (uuid === scriptRes.uuid || storageName === getStorageName(scriptRes)) { - const valueStore = scriptRes.value; + public valueStoreUpdate(valueStore: ValueStore, responses: ValueUpdateDataEncoded[]) { + // ----- 更新 valueStore (同步) ----- + for (const response of responses) { + const { id, valueChanges, sender } = response; const remote = sender.runFlag !== this.runFlag; if (!remote && id) { const fn = valueChangePromiseMap.get(id); @@ -163,20 +190,19 @@ class GM_Base implements IGM_Base { fn(); } } - if (valueUpdated) { - const valueChanges = entries; + const isUpdated = valueChanges.length > 0; + if (isUpdated) { for (const [key, rTyped1, rTyped2] of valueChanges) { const value = decodeRValue(rTyped1); const oldValue = decodeRValue(rTyped2); // 触发,并更新值 - if (value === undefined) { - if (valueStore[key] !== undefined) { - delete valueStore[key]; - } - } else { + if (value !== undefined) { valueStore[key] = value; + } else if (valueStore[key] !== undefined) { + delete valueStore[key]; } - this.valueChangeListener.execute(key, oldValue, value, remote, sender.tabId); + // emitToListener 会在下一个 microTask 才执行,避免影响 valueUpdate + emitToListener(this, key, oldValue, value, remote, sender.tabId); } } } @@ -257,44 +283,41 @@ export default class GMApi extends GM_Base { }); } - static _GM_setValue(a: GMApi, promise: any, key: string, value: any) { + static _GM_setValue(a: GMApi, promiseResolve: PromiseResolve, key: string, value: any) { if (!a.scriptRes) return; - if (valChangeCounterId > 1e8) { - // 防止 valChangeCounterId 过大导致无法正常工作 - valChangeCounterId = 0; - valChangeRandomId = `${randNum(8e11, 2e12).toString(36)}`; - } - const id = `${valChangeRandomId}::${++valChangeCounterId}`; - if (promise) { - valueChangePromiseMap.set(id, promise); + const id = generateValChangeId(); + if (promiseResolve) { + valueChangePromiseMap.set(id, promiseResolve); } // 对object的value进行一次转化 if (value && typeof value === "object") { value = JSON.parse(JSON.stringify(value)); } + const valueStore = a.scriptRes.value; + if (!a.extValueStoreCopy) { + a.extValueStoreCopy = { ...valueStore }; + } if (value === undefined) { - delete a.scriptRes.value[key]; + delete valueStore[key]; a.sendMessage("GM_setValue", [id, key]); } else { - a.scriptRes.value[key] = value; + valueStore[key] = value; a.sendMessage("GM_setValue", [id, key, value]); } return id; } - static _GM_setValues(a: GMApi, promise: any, values: TGMKeyValue) { + static _GM_setValues(a: GMApi, promiseResolve: PromiseResolve, values: TGMKeyValue) { if (!a.scriptRes) return; - if (valChangeCounterId > 1e8) { - // 防止 valChangeCounterId 过大导致无法正常工作 - valChangeCounterId = 0; - valChangeRandomId = `${randNum(8e11, 2e12).toString(36)}`; - } - const id = `${valChangeRandomId}::${++valChangeCounterId}`; - if (promise) { - valueChangePromiseMap.set(id, promise); + const id = generateValChangeId(); + if (promiseResolve) { + valueChangePromiseMap.set(id, promiseResolve); } const valueStore = a.scriptRes.value; const keyValuePairs = [] as [string, REncoded][]; + if (!a.extValueStoreCopy) { + a.extValueStoreCopy = { ...valueStore }; + } for (const [key, value] of Object.entries(values)) { let value_ = value; // 对object的value进行一次转化 diff --git a/src/app/service/content/inject.ts b/src/app/service/content/inject.ts index a503529bb..cdeea9f4b 100644 --- a/src/app/service/content/inject.ts +++ b/src/app/service/content/inject.ts @@ -5,7 +5,7 @@ import { sendMessage } from "@Packages/message/client"; import type { ScriptExecutor } from "./script_executor"; import type { TScriptInfo } from "@App/app/repo/scripts"; import type { EmitEventRequest } from "../service_worker/types"; -import type { GMInfoEnv, ValueUpdateDataEncoded } from "./types"; +import type { GMInfoEnv, ValueUpdateSendData } from "./types"; export class InjectRuntime { constructor( @@ -19,7 +19,7 @@ export class InjectRuntime { // 转发给脚本 this.scriptExecutor.emitEvent(data); }); - this.server.on("runtime/valueUpdate", (data: ValueUpdateDataEncoded) => { + this.server.on("runtime/valueUpdate", (data: ValueUpdateSendData) => { this.scriptExecutor.valueUpdate(data); }); } diff --git a/src/app/service/content/script_executor.ts b/src/app/service/content/script_executor.ts index 1a79c39b3..f8f3dbc97 100644 --- a/src/app/service/content/script_executor.ts +++ b/src/app/service/content/script_executor.ts @@ -1,8 +1,7 @@ import type { Message } from "@Packages/message/types"; -import { getStorageName } from "@App/pkg/utils/utils"; import type { EmitEventRequest } from "../service_worker/types"; import ExecScript from "./exec_script"; -import type { GMInfoEnv, ScriptFunc, ValueUpdateDataEncoded } from "./types"; +import type { GMInfoEnv, ScriptFunc, ValueUpdateSendData } from "./types"; import { addStyleSheet, definePropertyListener } from "./utils"; import type { ScriptLoadInfo, TScriptInfo } from "@App/app/repo/scripts"; import { DefinedFlags } from "../service_worker/runtime.consts"; @@ -35,23 +34,23 @@ try { // 脚本执行器 export class ScriptExecutor { earlyScriptFlag: Set = new Set(); - execMap: Map = new Map(); + execScriptMap: Map = new Map(); constructor(private msg: Message) {} emitEvent(data: EmitEventRequest) { // 转发给脚本 - const exec = this.execMap.get(data.uuid); + const exec = this.execScriptMap.get(data.uuid); if (exec) { exec.emitEvent(data.event, data.eventId, data.data); } } - valueUpdate(data: ValueUpdateDataEncoded) { - const { uuid, storageName } = data; - for (const val of this.execMap.values()) { - if (val.scriptRes.uuid === uuid || getStorageName(val.scriptRes) === storageName) { - val.valueUpdate(data); + valueUpdate(sendData: ValueUpdateSendData) { + const { storageName, storageChanges } = sendData; + for (const [uuid, responses] of Object.entries(storageChanges)) { + for (const execScript of this.execScriptMap.values()) { + execScript.valueUpdate(storageName, uuid, responses); } } } @@ -70,7 +69,7 @@ export class ScriptExecutor { const flag = script.flag; // 如果是EarlyScriptFlag,处理沙盒环境 if (this.earlyScriptFlag.has(flag)) { - for (const val of this.execMap.values()) { + for (const val of this.execScriptMap.values()) { if (val.scriptRes.flag === flag) { // 处理早期脚本的沙盒环境 val.updateEarlyScriptGMInfo(envInfo); @@ -138,7 +137,7 @@ export class ScriptExecutor { const { scriptLoadInfo, scriptFunc, envInfo } = scriptEntry; const exec = new ExecScript(scriptLoadInfo, "content", this.msg, scriptFunc, envInfo); - this.execMap.set(scriptLoadInfo.uuid, exec); + this.execScriptMap.set(scriptLoadInfo.uuid, exec); const metadata = scriptLoadInfo.metadata || {}; const resource = scriptLoadInfo.resource; // 注入css diff --git a/src/app/service/content/types.ts b/src/app/service/content/types.ts index 30fe88e80..57c3d4643 100644 --- a/src/app/service/content/types.ts +++ b/src/app/service/content/types.ts @@ -15,21 +15,18 @@ export type ValueUpdateSender = { export type ValueUpdateDataEntry = [string, any, any]; export type ValueUpdateDataREntry = [string, REncoded, REncoded]; -export type ValueUpdateData = { +export type ValueUpdateDataEncoded = { id?: string; - entries: ValueUpdateDataEntry[]; + valueChanges: ValueUpdateDataREntry[]; uuid: string; storageName: string; // 储存name sender: ValueUpdateSender; + updatetime: number; }; -export type ValueUpdateDataEncoded = { - id?: string; - entries: ValueUpdateDataREntry[]; - uuid: string; - storageName: string; // 储存name - sender: ValueUpdateSender; - valueUpdated: boolean; +export type ValueUpdateSendData = { + storageName: string; + storageChanges: Record; }; // gm_api.ts diff --git a/src/app/service/queue.ts b/src/app/service/queue.ts index c9c2a2b74..02ff77e79 100644 --- a/src/app/service/queue.ts +++ b/src/app/service/queue.ts @@ -1,4 +1,4 @@ -import type { Script, SCRIPT_RUN_STATUS, SCRIPT_STATUS, SCRIPT_TYPE } from "../repo/scripts"; +import type { SCRIPT_RUN_STATUS, SCRIPT_STATUS, SCRIPT_TYPE } from "../repo/scripts"; import type { InstallSource, SWScriptMenuItemOption, @@ -30,7 +30,7 @@ export type TEnableScript = { uuid: string; enable: boolean }; export type TScriptRunStatus = { uuid: string; runStatus: SCRIPT_RUN_STATUS }; -export type TScriptValueUpdate = { script: Script; valueUpdated: boolean }; +export type TScriptValueUpdate = { uuid: string; valueUpdated: boolean; status: SCRIPT_STATUS; isEarlyStart: boolean }; export type TScriptMenuRegister = { uuid: string; diff --git a/src/app/service/sandbox/runtime.ts b/src/app/service/sandbox/runtime.ts index 222fe2e66..39ca65129 100644 --- a/src/app/service/sandbox/runtime.ts +++ b/src/app/service/sandbox/runtime.ts @@ -13,7 +13,7 @@ import { CronJob } from "cron"; import { proxyUpdateRunStatus } from "../offscreen/client"; import { BgExecScriptWarp } from "../content/exec_warp"; import type ExecScript from "../content/exec_script"; -import type { ValueUpdateDataEncoded } from "../content/types"; +import type { ValueUpdateSendData } from "../content/types"; import { getStorageName, getMetadataStr, getUserConfigStr } from "@App/pkg/utils/utils"; import type { EmitEventRequest, ScriptLoadInfo } from "../service_worker/types"; import { CATRetryError } from "../content/exec_warp"; @@ -23,7 +23,7 @@ import { decodeRValue } from "@App/pkg/utils/message_value"; export class Runtime { cronJob: Map> = new Map(); - execScripts: Map = new Map(); + execScriptMap: Map = new Map(); logger: Logger; @@ -81,7 +81,7 @@ export class Runtime { async enableScript(script: ScriptRunResource) { // 开启脚本 // 如果正在运行,先释放 - if (this.execScripts.has(script.uuid)) { + if (this.execScriptMap.has(script.uuid)) { await this.disableScript(script.uuid); } const metadataStr = getMetadataStr(script.code) || ""; @@ -112,7 +112,7 @@ export class Runtime { } // 移除重试队列 this.removeRetryList(uuid); - if (!this.execScripts.has(uuid)) { + if (!this.execScriptMap.has(uuid)) { // 没有在运行 return false; } @@ -123,13 +123,13 @@ export class Runtime { // 执行脚本 async execScript(script: ScriptLoadInfo, execOnce?: boolean) { const logger = this.logger.with({ uuid: script.uuid, name: script.name }); - if (this.execScripts.has(script.uuid)) { + if (this.execScriptMap.has(script.uuid)) { // 释放掉资源 // 暂未实现执行完成后立马释放,会在下一次执行时释放 await this.stopScript(script.uuid); } const exec = new BgExecScriptWarp(script, this.windowMessage); - this.execScripts.set(script.uuid, exec); + this.execScriptMap.set(script.uuid, exec); proxyUpdateRunStatus(this.windowMessage, { uuid: script.uuid, runStatus: SCRIPT_RUN_STATUS_RUNNING }); // 修改掉脚本掉最后运行时间, 数据库也需要修改 script.lastruntime = Date.now(); @@ -294,19 +294,19 @@ export class Runtime { } async stopScript(uuid: string) { - const exec = this.execScripts.get(uuid); + const exec = this.execScriptMap.get(uuid); if (!exec) { proxyUpdateRunStatus(this.windowMessage, { uuid: uuid, runStatus: SCRIPT_RUN_STATUS_COMPLETE }); return false; } exec.stop(); - this.execScripts.delete(uuid); + this.execScriptMap.delete(uuid); proxyUpdateRunStatus(this.windowMessage, { uuid: uuid, runStatus: SCRIPT_RUN_STATUS_COMPLETE }); return true; } async runScript(script: ScriptRunResource) { - const exec = this.execScripts.get(script.uuid); + const exec = this.execScriptMap.get(script.uuid); // 如果正在运行,先释放 if (exec) { await this.stopScript(script.uuid); @@ -323,20 +323,27 @@ export class Runtime { return this.execScript(loadScript, true); } - valueUpdate(data: ValueUpdateDataEncoded) { - const dataEntries = data.entries; - // 转发给脚本 - this.execScripts.forEach((val) => { - if (val.scriptRes.uuid === data.uuid || getStorageName(val.scriptRes) === data.storageName) { - val.valueUpdate(data); + valueUpdate(sendData: ValueUpdateSendData) { + const { storageName, storageChanges } = sendData; + for (const [uuid, responses] of Object.entries(storageChanges)) { + // 转发给脚本 + for (const execScript of this.execScriptMap.values()) { + execScript.valueUpdate(storageName, uuid, responses); } - }); - // 更新crontabScripts中的脚本值 - for (const script of this.crontabSripts) { - if (script.uuid === data.uuid || getStorageName(script) === data.storageName) { - for (const [key, rTyped1, _rTyped2] of dataEntries) { - const value = decodeRValue(rTyped1); - script.value[key] = value; + for (const { valueChanges } of responses) { + // 更新crontabScripts中的脚本值 + for (const script of this.crontabSripts) { + if (script.uuid === uuid || getStorageName(script) === storageName) { + const valueStore = script.value; + for (const [key, rTyped1, _rTyped2] of valueChanges) { + const value = decodeRValue(rTyped1); + if (value !== undefined) { + valueStore[key] = value; + } else if (valueStore[key] !== undefined) { + delete valueStore[key]; + } + } + } } } } @@ -344,7 +351,7 @@ export class Runtime { emitEvent(data: EmitEventRequest) { // 转发给脚本 - const exec = this.execScripts.get(data.uuid); + const exec = this.execScriptMap.get(data.uuid); if (exec) { exec.emitEvent(data.event, data.eventId, data.data); } diff --git a/src/app/service/service_worker/gm_api/gm_api.ts b/src/app/service/service_worker/gm_api/gm_api.ts index 1e5135605..5eb89b296 100644 --- a/src/app/service/service_worker/gm_api/gm_api.ts +++ b/src/app/service/service_worker/gm_api/gm_api.ts @@ -427,7 +427,7 @@ export default class GMApi { await this.value.setValues({ uuid: request.script.uuid, id, keyValuePairs, isReplace: false, valueSender }); } - @PermissionVerify.API({ link: ["GM_deleteValues"] }) + @PermissionVerify.API({ link: ["GM_deleteValue", "GM_deleteValues"] }) async GM_setValues(request: GMApiRequest<[string, TKeyValuePair[]]>, sender: IGetSender) { if (!request.params || request.params.length !== 2) { throw new Error("param is failed"); diff --git a/src/app/service/service_worker/permission_verify.ts b/src/app/service/service_worker/permission_verify.ts index 2beefcaaf..2725a8f1f 100644 --- a/src/app/service/service_worker/permission_verify.ts +++ b/src/app/service/service_worker/permission_verify.ts @@ -117,7 +117,12 @@ export default class PermissionVerify { } // 验证是否有权限 - async verify(request: GMApiRequest, api: ApiValue, sender: IGetSender, GMApiInstance: GMApi): Promise { + async verify>( + request: GMApiRequest, + api: ApiValue, + sender: IGetSender, + GMApiInstance: GMApi + ): Promise { const { alias, link, confirm } = api.param; if (api.param.default) { return true; diff --git a/src/app/service/service_worker/runtime.ts b/src/app/service/service_worker/runtime.ts index a918ff726..b828a927f 100644 --- a/src/app/service/service_worker/runtime.ts +++ b/src/app/service/service_worker/runtime.ts @@ -458,15 +458,22 @@ export class RuntimeService { }); // 监听脚本值变更 - this.mq.subscribe("valueUpdate", async ({ script, valueUpdated }: TScriptValueUpdate) => { - if (valueUpdated) { - if (script.status === SCRIPT_STATUS_ENABLE && isEarlyStartScript(script.metadata)) { - // 如果是预加载脚本,需要更新脚本代码重新注册 - // scriptMatchInfo 里的 value 改变 => compileInjectionCode -> injectionCode 改变 - await this.updateResourceOnScriptChange(script); + this.mq.subscribe( + "valueUpdate", + async ({ uuid, valueUpdated, status, isEarlyStart }: TScriptValueUpdate) => { + if (valueUpdated) { + if (status === SCRIPT_STATUS_ENABLE && isEarlyStart) { + // 如果是预加载脚本,需要更新脚本代码重新注册 + // scriptMatchInfo 里的 value 改变 => compileInjectionCode -> injectionCode 改变 + const script = await this.scriptDAO.get(uuid); + // 因為從 scriptDAO 取了最新的。所以再確認一下吧。 + if (script && script.status === SCRIPT_STATUS_ENABLE && isEarlyStartScript(script.metadata)) { + await this.updateResourceOnScriptChange(script); + } + } } } - }); + ); if (chrome.extension.inIncognitoContext) { this.systemConfig.addListener("enable_script_incognito", async (enable) => { diff --git a/src/app/service/service_worker/value.test.ts b/src/app/service/service_worker/value.test.ts index bd28eaaec..9ab7d3300 100644 --- a/src/app/service/service_worker/value.test.ts +++ b/src/app/service/service_worker/value.test.ts @@ -13,12 +13,31 @@ import { Server } from "@Packages/message/server"; import EventEmitter from "eventemitter3"; import { MessageQueue } from "@Packages/message/message_queue"; import type { ValueUpdateSender } from "../content/types"; -import { getStorageName } from "@App/pkg/utils/utils"; -import type { TKeyValuePair } from "@App/pkg/utils/message_value"; -import { encodeRValue } from "@App/pkg/utils/message_value"; +import { deferred, getStorageName } from "@App/pkg/utils/utils"; +import { type TScriptValueUpdate } from "../queue"; +import { isEarlyStartScript } from "../content/utils"; +import { CACHE_KEY_SET_VALUE } from "@App/app/cache_key"; +import { stackAsyncTask } from "@App/pkg/utils/async_queue"; +import { encodeRValue, type TKeyValuePair } from "@App/pkg/utils/message_value"; initTestEnv(); +const nextTick = () => Promise.resolve(); +const flush = async () => { + await nextTick(); + await nextTick(); +}; + +const expectedValueUpdateEventEmit = (mockScript: Script, valueUpdated: boolean): TScriptValueUpdate => { + const valueUpdateEventEmit: TScriptValueUpdate = { + uuid: mockScript.uuid, + valueUpdated, + status: mockScript.status, + isEarlyStart: isEarlyStartScript(mockScript.metadata), + }; + return valueUpdateEventEmit; +}; + /** * ValueService.setValue 方法的单元测试 * @@ -113,6 +132,7 @@ describe("ValueService - setValue 方法测试", () => { valueSender: mockSender, isReplace: false, }); + await flush(); // 验证结果 expect(mockScriptDAO.get).toHaveBeenCalledWith(mockScript.uuid); @@ -122,19 +142,26 @@ describe("ValueService - setValue 方法测试", () => { expect(valueService.pushValueToTab).toHaveBeenNthCalledWith( 1, expect.objectContaining({ - entries: expect.any(Object), - id: "testId-4021", - sender: expect.objectContaining({ - runFlag: expect.any(String), - tabId: expect.any(Number), - }), storageName: getStorageName(mockScript), - uuid: mockScript.uuid, - valueUpdated: true, + storageChanges: expect.objectContaining({ + [mockScript.uuid]: [ + expect.objectContaining({ + valueChanges: Array(1).fill(expect.anything()), + id: "testId-4021", + sender: expect.objectContaining({ + runFlag: expect.any(String), + tabId: expect.any(Number), + }), + storageName: getStorageName(mockScript), + updatetime: expect.any(Number), + uuid: mockScript.uuid, + }), + ], + }), }) ); expect(mockMessageQueue.emit).toHaveBeenCalledTimes(1); - expect(mockMessageQueue.emit).toHaveBeenCalledWith("valueUpdate", { script: mockScript, valueUpdated: true }); + expect(mockMessageQueue.emit).toHaveBeenCalledWith("valueUpdate", expectedValueUpdateEventEmit(mockScript, true)); // 验证保存的数据结构 const saveCall = vi.mocked(mockValueDAO.save).mock.calls[0]; @@ -166,6 +193,7 @@ describe("ValueService - setValue 方法测试", () => { valueSender: mockSender, isReplace: false, }); + await flush(); // 验证结果 expect(mockScriptDAO.get).toHaveBeenCalledWith(mockScript.uuid); @@ -175,19 +203,25 @@ describe("ValueService - setValue 方法测试", () => { expect(valueService.pushValueToTab).toHaveBeenNthCalledWith( 1, expect.objectContaining({ - entries: expect.any(Object), - id: "testId-4022", - sender: expect.objectContaining({ - runFlag: expect.any(String), - tabId: expect.any(Number), - }), storageName: getStorageName(mockScript), - uuid: mockScript.uuid, - valueUpdated: true, + storageChanges: expect.objectContaining({ + [mockScript.uuid]: [ + expect.objectContaining({ + valueChanges: Array(1).fill(expect.anything()), + id: "testId-4022", + sender: expect.objectContaining({ + runFlag: expect.any(String), + tabId: expect.any(Number), + }), + storageName: getStorageName(mockScript), + uuid: mockScript.uuid, + }), + ], + }), }) ); expect(mockMessageQueue.emit).toHaveBeenCalledTimes(1); - expect(mockMessageQueue.emit).toHaveBeenCalledWith("valueUpdate", { script: mockScript, valueUpdated: true }); + expect(mockMessageQueue.emit).toHaveBeenCalledWith("valueUpdate", expectedValueUpdateEventEmit(mockScript, true)); // 验证保存的数据结构 const saveCall = vi.mocked(mockValueDAO.save).mock.calls[0]; @@ -228,6 +262,7 @@ describe("ValueService - setValue 方法测试", () => { valueSender: mockSender, isReplace: false, }); + await flush(); // 验证结果 expect(mockScriptDAO.get).toHaveBeenCalledWith(mockScript.uuid); @@ -237,22 +272,25 @@ describe("ValueService - setValue 方法测试", () => { expect(valueService.pushValueToTab).toHaveBeenNthCalledWith( 1, expect.objectContaining({ - entries: expect.any(Object), - id: "testId-4023", - sender: expect.objectContaining({ - runFlag: expect.any(String), - tabId: expect.any(Number), - }), storageName: getStorageName(mockScript), - uuid: mockScript.uuid, - valueUpdated: true, + storageChanges: expect.objectContaining({ + [mockScript.uuid]: [ + expect.objectContaining({ + valueChanges: Array(1).fill(expect.anything()), + id: "testId-4023", + sender: expect.objectContaining({ + runFlag: expect.any(String), + tabId: expect.any(Number), + }), + storageName: getStorageName(mockScript), + uuid: mockScript.uuid, + }), + ], + }), }) ); expect(mockMessageQueue.emit).toHaveBeenCalledTimes(1); - expect(mockMessageQueue.emit).toHaveBeenCalledWith("valueUpdate", { - script: mockScript, - valueUpdated: true, - }); + expect(mockMessageQueue.emit).toHaveBeenCalledWith("valueUpdate", expectedValueUpdateEventEmit(mockScript, true)); // 验证保存的数据被正确更新 const saveCall = vi.mocked(mockValueDAO.save).mock.calls[0]; @@ -289,6 +327,7 @@ describe("ValueService - setValue 方法测试", () => { valueSender: mockSender, isReplace: false, }); + await flush(); // 验证结果 - 不应该保存或发送更新 expect(mockScriptDAO.get).toHaveBeenCalledWith(mockScript.uuid); @@ -298,19 +337,25 @@ describe("ValueService - setValue 方法测试", () => { expect(valueService.pushValueToTab).toHaveBeenNthCalledWith( 1, expect.objectContaining({ - entries: expect.any(Object), - id: "testId-4024", - sender: expect.objectContaining({ - runFlag: expect.any(String), - tabId: expect.any(Number), - }), storageName: getStorageName(mockScript), - uuid: mockScript.uuid, - valueUpdated: false, + storageChanges: expect.objectContaining({ + [mockScript.uuid]: [ + expect.objectContaining({ + valueChanges: Array(0), + id: "testId-4024", + sender: expect.objectContaining({ + runFlag: expect.any(String), + tabId: expect.any(Number), + }), + storageName: getStorageName(mockScript), + uuid: mockScript.uuid, + }), + ], + }), }) ); // 值未改变 expect(mockMessageQueue.emit).toHaveBeenCalledTimes(1); - expect(mockMessageQueue.emit).toHaveBeenCalledWith("valueUpdate", { script: mockScript, valueUpdated: false }); // 值未改变 + expect(mockMessageQueue.emit).toHaveBeenCalledWith("valueUpdate", expectedValueUpdateEventEmit(mockScript, false)); // 值未改变 }); it("当设置值为undefined时应该删除该键", async () => { @@ -341,6 +386,7 @@ describe("ValueService - setValue 方法测试", () => { valueSender: mockSender, isReplace: false, }); + await flush(); // 验证结果 expect(mockValueDAO.save).toHaveBeenCalled(); @@ -371,6 +417,7 @@ describe("ValueService - setValue 方法测试", () => { isReplace: false, }) ).rejects.toThrow("script not found"); + await flush(); // 验证不会执行后续操作 expect(mockValueDAO.get).not.toHaveBeenCalled(); @@ -396,10 +443,13 @@ describe("ValueService - setValue 方法测试", () => { expect(mockValueDAO.save).toHaveBeenCalledTimes(0); expect(valueService.pushValueToTab).toHaveBeenCalledTimes(0); + const d = deferred(); + stackAsyncTask(`${CACHE_KEY_SET_VALUE}${getStorageName(mockScript)}`, () => d.promise); + // 并发执行两个setValue操作 const keyValuePairs1 = [[key1, encodeRValue(value1)]] satisfies TKeyValuePair[]; const keyValuePairs2 = [[key2, encodeRValue(value2)]] satisfies TKeyValuePair[]; - await Promise.all([ + const ret = Promise.all([ valueService.setValues({ uuid: mockScript.uuid, id: "testId-4041", @@ -415,41 +465,52 @@ describe("ValueService - setValue 方法测试", () => { isReplace: false, }), ]); + await flush(); + d.resolve(); + await flush(); + await ret; + await flush(); // 验证两个操作都被调用 expect(mockScriptDAO.get).toHaveBeenCalledTimes(2); - expect(mockValueDAO.save).toHaveBeenCalledTimes(2); - expect(valueService.pushValueToTab).toHaveBeenCalledTimes(2); + expect(mockValueDAO.get).toHaveBeenCalledTimes(1); + expect(mockValueDAO.save).toHaveBeenCalledTimes(1); + expect(valueService.pushValueToTab).toHaveBeenCalledTimes(1); expect(valueService.pushValueToTab).toHaveBeenNthCalledWith( 1, expect.objectContaining({ - entries: expect.any(Object), - id: "testId-4041", - sender: expect.objectContaining({ - runFlag: expect.any(String), - tabId: expect.any(Number), - }), storageName: getStorageName(mockScript), - uuid: mockScript.uuid, - valueUpdated: true, - }) - ); - expect(valueService.pushValueToTab).toHaveBeenNthCalledWith( - 2, - expect.objectContaining({ - entries: expect.any(Object), - id: "testId-4042", - sender: expect.objectContaining({ - runFlag: expect.any(String), - tabId: expect.any(Number), + storageChanges: expect.objectContaining({ + [mockScript.uuid]: [ + expect.objectContaining({ + valueChanges: Array(1).fill(expect.anything()), + id: "testId-4041", + sender: expect.objectContaining({ + runFlag: expect.any(String), + tabId: expect.any(Number), + }), + storageName: getStorageName(mockScript), + uuid: mockScript.uuid, + }), + expect.objectContaining({ + valueChanges: Array(1).fill(expect.anything()), + id: "testId-4042", + sender: expect.objectContaining({ + runFlag: expect.any(String), + tabId: expect.any(Number), + }), + storageName: getStorageName(mockScript), + uuid: mockScript.uuid, + }), + ], }), - storageName: getStorageName(mockScript), - uuid: mockScript.uuid, - valueUpdated: true, }) ); - expect(mockMessageQueue.emit).toHaveBeenCalledTimes(2); - expect(mockMessageQueue.emit).toHaveBeenNthCalledWith(1, "valueUpdate", { script: mockScript, valueUpdated: true }); - expect(mockMessageQueue.emit).toHaveBeenNthCalledWith(2, "valueUpdate", { script: mockScript, valueUpdated: true }); + expect(mockMessageQueue.emit).toHaveBeenCalledTimes(1); + expect(mockMessageQueue.emit).toHaveBeenNthCalledWith( + 1, + "valueUpdate", + expectedValueUpdateEventEmit(mockScript, true) + ); }); }); diff --git a/src/app/service/service_worker/value.ts b/src/app/service/service_worker/value.ts index 42f539294..e1342a328 100644 --- a/src/app/service/service_worker/value.ts +++ b/src/app/service/service_worker/value.ts @@ -1,12 +1,17 @@ import LoggerCore from "@App/app/logger/core"; import type Logger from "@App/app/logger/logger"; -import { type Script, ScriptDAO } from "@App/app/repo/scripts"; +import { type Script, type SCRIPT_STATUS, ScriptDAO, type ValueStore } from "@App/app/repo/scripts"; import { type Value, ValueDAO } from "@App/app/repo/value"; import type { IGetSender, Group } from "@Packages/message/server"; import { type RuntimeService } from "./runtime"; import { type PopupService } from "./popup"; -import { getStorageName } from "@App/pkg/utils/utils"; -import type { ValueUpdateDataEncoded, ValueUpdateDataREntry, ValueUpdateSender } from "../content/types"; +import { aNow, getStorageName } from "@App/pkg/utils/utils"; +import type { + ValueUpdateDataEncoded, + ValueUpdateDataREntry, + ValueUpdateSendData, + ValueUpdateSender, +} from "../content/types"; import type { TScriptValueUpdate } from "../queue"; import { type TDeleteScript } from "../queue"; import { type IMessageQueue } from "@Packages/message/message_queue"; @@ -14,6 +19,19 @@ import { CACHE_KEY_SET_VALUE } from "@App/app/cache_key"; import { stackAsyncTask } from "@App/pkg/utils/async_queue"; import type { TKeyValuePair } from "@App/pkg/utils/message_value"; import { decodeRValue, R_UNDEFINED, encodeRValue } from "@App/pkg/utils/message_value"; +import { isEarlyStartScript } from "../content/utils"; + +type ValueUpdateTaskInfo = { + uuid: string; + id: string; + keyValuePairs: TKeyValuePair[]; + valueSender: ValueUpdateSender; + isReplace: boolean; + ts: number; + status: SCRIPT_STATUS; + isEarlyStart: boolean; +}; +const valueUpdateTasks = new Map(); export type TSetValuesParams = { uuid: string; @@ -76,8 +94,19 @@ export class ValueService { } // 推送值到tab - async pushValueToTab(sendData: T) { - const { storageName } = sendData; + async pushValueToTab(sendData: ValueUpdateSendData) { + const storageName = sendData.storageName; + /* + --- data structure --- + { + storageName: XXXX + { + uuid1: data1 + uuid2: data2 + ... + } + } + */ chrome.tabs.query({}, (tabs) => { const lastError = chrome.runtime.lastError; if (lastError) { @@ -112,37 +141,30 @@ export class ValueService { ); } - // 批量设置 - async setValues(params: TSetValuesParams) { - const { uuid, keyValuePairs, isReplace } = params; - const id = params.id || ""; - const ts = params.ts || 0; - const valueSender = params.valueSender || { - runFlag: "user", - tabId: -2, - }; - // 查询出脚本 - const script = await this.scriptDAO.get(uuid); - if (!script) { - throw new Error("script not found"); - } - // 查询老的值 - const storageName = getStorageName(script); - let oldValueRecord: { [key: string]: any } = {}; - const cacheKey = `${CACHE_KEY_SET_VALUE}${storageName}`; - const entries = [] as ValueUpdateDataREntry[]; - const _flag = await stackAsyncTask(cacheKey, async () => { - let valueModel: Value | undefined = await this.valueDAO.get(storageName); + async setValuesByStorageName(storageName: string) { + const taskListRef = valueUpdateTasks.get(storageName); + if (!taskListRef?.length) return; + const valueUpdateEmits = new Map(); + let valueModel: Value | undefined = await this.valueDAO.get(storageName); + const taskList = taskListRef.slice(0); + taskListRef.length = 0; + valueUpdateTasks.delete(storageName); + // ------ 读取 & 更新 ------ + let updatetime = 0; + const storageChanges: Record = {}; + let valueModelUpdated = false; + let hasValueUpdated = false; + for (const task of taskList) { + const entries = [] as ValueUpdateDataREntry[]; + const { uuid, keyValuePairs, isReplace, ts, status, isEarlyStart } = task; + valueUpdateEmits.set(uuid, { status, isEarlyStart }); // 针对各脚本发送结果。uuid重复则忽略 + let oldValueRecord: ValueStore = {}; + const now = aNow(); // 保证严格递增 + let changed = false; + let dataModel: ValueStore; if (!valueModel) { - const now = Date.now(); - const dataModel: { [key: string]: any } = {}; - for (const [key, rTyped1] of keyValuePairs) { - const value = decodeRValue(rTyped1); - if (value !== undefined) { - dataModel[key] = value; - entries.push([key, rTyped1, R_UNDEFINED]); - } - } + changed = true; + dataModel = {}; // 即使是空 dataModel 也进行更新 // 由于没entries, valueUpdated 是 false, 但 valueDAO 会有一个空的 valueModel 记录 updatetime valueModel = { @@ -153,54 +175,116 @@ export class ValueService { updatetime: ts ? Math.min(ts, now) : now, }; } else { - let changed = false; - let dataModel = (oldValueRecord = valueModel.data); - dataModel = { ...dataModel }; // 每次储存使用新参考 - const containedKeys = new Set(); - for (const [key, rTyped1] of keyValuePairs) { - containedKeys.add(key); - const value = decodeRValue(rTyped1); - const oldValue = dataModel[key]; - if (oldValue === value) continue; - changed = true; - if (value === undefined) { - delete dataModel[key]; - } else { - dataModel[key] = value; - } - const rTyped2 = encodeRValue(oldValue); - entries.push([key, rTyped1, rTyped2]); + oldValueRecord = valueModel.data; + dataModel = { ...oldValueRecord }; // 每次储存使用新参考 + } + const containedKeys = new Set(); + for (const [key, rTyped1] of keyValuePairs) { + containedKeys.add(key); + const value = decodeRValue(rTyped1); + const oldValue = dataModel[key]; + if (oldValue === value) continue; + changed = true; + if (value === undefined) { + delete dataModel[key]; + } else { + dataModel[key] = value; } - if (isReplace) { - // 处理oldValue有但是没有在data.values中的情况 - for (const key of Object.keys(oldValueRecord)) { - if (!containedKeys.has(key)) { - changed = true; - const oldValue = oldValueRecord[key]; - delete dataModel[key]; // 这里使用delete是因为保存不需要这个字段了 - const rTyped2 = encodeRValue(oldValue); - entries.push([key, R_UNDEFINED, rTyped2]); - } + const rTyped2 = encodeRValue(oldValue); + entries.push([key, rTyped1, rTyped2]); + } + if (isReplace) { + // 处理oldValue有但是没有在data.values中的情况 + for (const key of Object.keys(oldValueRecord)) { + if (!containedKeys.has(key)) { + changed = true; + const oldValue = oldValueRecord[key]; + delete dataModel[key]; // 这里使用delete是因为保存不需要这个字段了 + const rTyped2 = encodeRValue(oldValue); + entries.push([key, R_UNDEFINED, rTyped2]); } } - if (!changed) return false; + } + if (changed) { + valueModel.updatetime = now; valueModel.data = dataModel; // 每次储存使用新参考 + valueModelUpdated = true; } - await this.valueDAO.save(storageName, valueModel); - return true; - }); + updatetime = valueModel.updatetime; + + { + const { uuid, id, valueSender } = task; + let list = storageChanges[uuid]; + if (!list) { + storageChanges[uuid] = list = []; + } + const valueUpdated = entries.length > 0; + if (valueUpdated) hasValueUpdated = true; + list.push({ + id, + valueChanges: entries, + uuid, + storageName, + sender: valueSender, + updatetime, + } as ValueUpdateDataEncoded); + } + } + if (valueModelUpdated) { + await this.valueDAO.save(storageName, valueModel!); + } + // ------ 推送 ------ // 推送到所有加载了本脚本的tab中 - const valueUpdated = entries.length > 0; - this.pushValueToTab({ - id, - entries: entries, - uuid, - storageName, - sender: valueSender, - valueUpdated, - } as ValueUpdateDataEncoded); - // valueUpdate 消息用于 early script 的处理 - this.mq.emit("valueUpdate", { script, valueUpdated }); + this.pushValueToTab({ storageName, storageChanges }); + for (const [uuid, { status, isEarlyStart }] of valueUpdateEmits.entries()) { + // valueUpdate 消息用于 early script 的处理 + // 由于经过 await, 此处的 status 和 isEarlyStart 只供参考,应在接收端检查最新设置值 + this.mq.emit("valueUpdate", { + uuid, + valueUpdated: hasValueUpdated, + status, + isEarlyStart, + }); + } + } + + // 批量设置 + async setValues(params: TSetValuesParams): Promise { + // stackAsyncTask 确保 setValues的 taskList阵列新增次序正确 + let storageName: string; + let cacheKey: string; + const { uuid, keyValuePairs, isReplace } = params; + const id = params.id || ""; + const ts = params.ts || 0; + const valueSender = params.valueSender || { + runFlag: "user", + tabId: -2, + }; + await stackAsyncTask("valueChangeOnSequence", async () => { + // 查询出脚本 + const script = await this.scriptDAO.get(uuid); + if (!script) { + throw new Error("script not found"); + } + storageName = getStorageName(script); + cacheKey = `${CACHE_KEY_SET_VALUE}${storageName}`; + let taskList = valueUpdateTasks.get(storageName); + if (!taskList) { + valueUpdateTasks.set(storageName, (taskList = [])); + } + taskList.push({ + uuid, + id, + keyValuePairs, + valueSender, + isReplace, + ts, + status: script.status, + isEarlyStart: isEarlyStartScript(script.metadata), + }); + }); + // valueDAO 次序依 storageName + await stackAsyncTask(cacheKey!, () => this.setValuesByStorageName(storageName!)); } setScriptValues(params: Pick, _sender: IGetSender) {