diff --git a/src/app/service/content/exec_script.ts b/src/app/service/content/exec_script.ts index 9d54bf883..b5ff7c661 100644 --- a/src/app/service/content/exec_script.ts +++ b/src/app/service/content/exec_script.ts @@ -64,8 +64,8 @@ export default class ExecScript { this.sandboxContext?.emitEvent(event, eventId, data); } - valueUpdate(data: ValueUpdateDataEncoded) { - this.sandboxContext?.valueUpdate(data); + valueUpdate(storageName: string, uuid: string, data: ValueUpdateDataEncoded[]) { + this.sandboxContext?.valueUpdate(storageName, uuid, data); } 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..6f8687f2b 100644 --- a/src/app/service/content/gm_api/gm_api.test.ts +++ b/src/app/service/content/gm_api/gm_api.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it, vi } from "vitest"; +import { describe, expect, it, type Mock, vi } from "vitest"; import ExecScript from "../exec_script"; import type { ScriptLoadInfo } from "@App/app/service/service_worker/types"; import type { GMInfoEnv, ScriptFunc } from "../types"; @@ -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 = { @@ -107,6 +108,40 @@ describe.concurrent("window.*", () => { }); describe.concurrent("GM Api", () => { + const valueDaoUpdatetimeFix = ( + mockSendMessage: Mock<(...args: any[]) => any>, + exec: ExecScript, + script: ScriptLoadInfo + ) => { + const forceUpdateTimeRefreshIdx = mockSendMessage.mock.calls.findIndex((entry) => { + return entry?.[0]?.data?.api === "internalApiWaitForFreshValueState"; + }); + if (forceUpdateTimeRefreshIdx >= 0) { + const actualCall = mockSendMessage.mock.calls[forceUpdateTimeRefreshIdx][0]; + expect(mockSendMessage).toHaveBeenNthCalledWith( + forceUpdateTimeRefreshIdx + 1, + expect.objectContaining({ + action: "content/runtime/gmApi", + data: { + api: "internalApiWaitForFreshValueState", + params: [expect.stringMatching(/^.+::\d+$/)], + runFlag: expect.any(String), + uuid: undefined, + }, + }) + ); + exec.valueUpdate(getStorageName(script), script.uuid, [ + { + id: actualCall.data.params[0], + entries: [["TEST_NON_EXIST_REMOVAL", encodeRValue(undefined), encodeRValue(undefined)]], + uuid: script.uuid, + storageName: getStorageName(script), + sender: { runFlag: exec.sandboxContext!.runFlag, tabId: -2 }, + updatetime: Date.now(), + }, + ]); + } + }; it.concurrent("GM_getValue", async () => { const script = Object.assign({}, scriptRes) as ScriptLoadInfo; script.value = { test: "ok" }; @@ -123,10 +158,15 @@ 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(); + valueDaoUpdatetimeFix(mockSendMessage, exec, script); + const ret = await retPromise; expect(ret).toEqual("ok!"); }); @@ -135,10 +175,15 @@ 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(); + valueDaoUpdatetimeFix(mockSendMessage, exec, script); + const ret = await retPromise; expect(ret).toEqual("test1-test2-test3"); }); @@ -151,11 +196,16 @@ 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(); + valueDaoUpdatetimeFix(mockSendMessage, exec, script); + const ret = await retPromise; + expect(ret).toEqual("test5-test2-test3-test1"); // TM也没有sort }); it.concurrent("GM.listValues", async () => { @@ -163,10 +213,15 @@ 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(); + valueDaoUpdatetimeFix(mockSendMessage, exec, script); + const ret = await retPromise; expect(ret).toEqual("test1-test2-test3"); }); @@ -179,11 +234,16 @@ 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(); + valueDaoUpdatetimeFix(mockSendMessage, exec, script); + const ret = await retPromise; + expect(ret).toEqual("test5-test2-test3-test1"); // TM也没有sort }); it.concurrent("GM_getValues", async () => { @@ -212,10 +272,15 @@ 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(); + valueDaoUpdatetimeFix(mockSendMessage, exec, script); + const ret = await retPromise; expect(ret.test1).toEqual("23"); expect(ret.test2).toEqual(45); expect(ret.test3).toEqual("67"); @@ -499,7 +564,7 @@ describe.concurrent("GM_value", () => { api: "GM_setValues", params: [ // event id - expect.stringMatching(/^.+::\d$/), + expect.stringMatching(/^.+::\d+$/), // the object payload keyValuePairs1, ], @@ -523,7 +588,7 @@ describe.concurrent("GM_value", () => { api: "GM_setValues", params: [ // event id - expect.stringMatching(/^.+::\d$/), + expect.stringMatching(/^.+::\d+$/), // the object payload keyValuePairs2, ], @@ -573,7 +638,7 @@ describe.concurrent("GM_value", () => { api: "GM_setValues", params: [ // event id - expect.stringMatching(/^.+::\d$/), + expect.stringMatching(/^.+::\d+$/), // the object payload keyValuePairs1, ], @@ -592,7 +657,7 @@ describe.concurrent("GM_value", () => { api: "GM_setValue", params: [ // event id - expect.stringMatching(/^.+::\d$/), + expect.stringMatching(/^.+::\d+$/), // the string payload "b", ], @@ -643,7 +708,7 @@ describe.concurrent("GM_value", () => { api: "GM_setValues", params: [ // event id - expect.stringMatching(/^.+::\d$/), + expect.stringMatching(/^.+::\d+$/), // the object payload keyValuePairs1, ], @@ -667,7 +732,7 @@ describe.concurrent("GM_value", () => { api: "GM_setValues", params: [ // event id - expect.stringMatching(/^.+::\d$/), + expect.stringMatching(/^.+::\d+$/), // the string payload keyValuePairs2, ], @@ -702,14 +767,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", + entries: [["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 +804,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", + entries: [["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 +841,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, + entries: [["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 2238fe221..0e841fc34 100644 --- a/src/app/service/content/gm_api/gm_api.ts +++ b/src/app/service/content/gm_api/gm_api.ts @@ -10,7 +10,8 @@ import type { TScriptMenuItemKey, MessageRequest, } from "@App/app/service/service_worker/types"; -import { base64ToBlob, randNum, randomMessageFlag, strToBase64 } from "@App/pkg/utils/utils"; +import type { Deferred } from "@App/pkg/utils/utils"; +import { base64ToBlob, deferred, randNum, randomMessageFlag, strToBase64 } from "@App/pkg/utils/utils"; import LoggerCore from "@App/app/logger/core"; import EventEmitter from "eventemitter3"; import GMContext from "./gm_context"; @@ -23,12 +24,13 @@ import { decodeRValue, encodeRValue, type REncoded } from "@App/pkg/utils/messag import { type TGMKeyValue } from "@App/app/repo/value"; import type { ContextType } from "./gm_xhr"; import { convObjectToURL, GM_xmlhttpRequest, toBlobURL, urlToDocumentInContentPage } from "./gm_xhr"; +import { stackAsyncTask } from "@App/pkg/utils/async_queue"; // 内部函数呼叫定义 export interface IGM_Base { sendMessage(api: string, params: any[]): Promise; connect(api: string, params: any[]): Promise; - valueUpdate(data: ValueUpdateDataEncoded): void; + valueUpdate(storageName: string, uuid: string, data: ValueUpdateDataEncoded[]): void; emitEvent(event: string, eventId: string, data: any): void; } @@ -43,7 +45,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 +68,21 @@ const execEnvInit = (execEnv: GMApi) => { } }; +const emitToListener = ( + a: GM_Base, + key: string, + oldValue: any, + value: any, + remote: boolean, + tabId: number | undefined // 注: tabId 的提供不在标准 GM API 的定义 +) => { + // 在 valueUpdate 完成后才放行。避免卡住 valueUpdate 线程 + stackAsyncTask("valueUpdateEventListenerEmit", () => { + // 不等待结果 + 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 +102,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 +139,15 @@ class GM_Base implements IGM_Base { @GMContext.protected() public setInvalidContext!: () => void; + @GMContext.protected() + public readFreshes: Map> | undefined; + + @GMContext.protected() + public extValueStoreCopy: Record | undefined; // 使每个tab的valueChange次序保持一致 + + @GMContext.protected() + valueDaoUpdatetime: number | undefined; + // 单次回调使用 @GMContext.protected() public async sendMessage(api: string, params: any[]) { @@ -149,36 +187,65 @@ class GM_Base implements IGM_Base { } @GMContext.protected() - public valueUpdate(data: ValueUpdateDataEncoded) { - if (!this.scriptRes || !this.valueChangeListener) return; + public valueUpdate(storageName: string, uuid: string, list: ValueUpdateDataEncoded[]) { + if (!this.scriptRes) return; const scriptRes = this.scriptRes; - const { id, uuid, entries, storageName, sender, valueUpdated } = data; - if (uuid === scriptRes.uuid || storageName === getStorageName(scriptRes)) { + let lastUpdateTime = 0; + if (uuid == scriptRes.uuid || storageName === getStorageName(scriptRes)) { + const hold = deferred(); + // 避免立即 emit + stackAsyncTask("valueUpdateEventListenerEmit", () => hold.promise); + // ----- 更新 valueStore (同步) ----- + scriptRes.value = this.extValueStoreCopy || scriptRes.value; const valueStore = scriptRes.value; - const remote = sender.runFlag !== this.runFlag; - if (!remote && id) { - const fn = valueChangePromiseMap.get(id); - if (fn) { - valueChangePromiseMap.delete(id); - fn(); + for (const data of list) { + const { id, entries, sender, updatetime } = data; + const remote = sender.runFlag !== this.runFlag; + if (!remote && id) { + const fn = valueChangePromiseMap.get(id); + if (fn) { + valueChangePromiseMap.delete(id); + fn(); + } + } + const isUpdated = entries.length > 0; + if (isUpdated) { + const valueChanges = entries; + 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 { + valueStore[key] = value; + } + if (this.valueChangeListener) { + emitToListener(this, key, oldValue, value, remote, sender.tabId); + } + } + } + if (updatetime) { + lastUpdateTime = updatetime; } } - if (valueUpdated) { - const valueChanges = entries; - 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]; + this.extValueStoreCopy = { ...valueStore }; + // ----- 更新 valueStore (同步) ----- + if (lastUpdateTime) { + const readFreshes = this.readFreshes; + if (readFreshes) { + for (const [t, d] of readFreshes.entries()) { + if (lastUpdateTime >= t) { + readFreshes.delete(t); + d.resolve(lastUpdateTime); } - } else { - valueStore[key] = value; } - this.valueChangeListener.execute(key, oldValue, value, remote, sender.tabId); } + this.valueDaoUpdatetime = lastUpdateTime; } + hold.resolve(); // 放行 emit } } @@ -233,6 +300,37 @@ export default class GMApi extends GM_Base { ); } + static async waitForFreshValueState(a: GMApi): Promise { + // 读取前没有任何 valueUpdate 的话,valueDaoUpdatetime 为 undefined + // valueDaoUpdatetime 需透过 valueUpdate 提供 (不要从其他途径影响页面缓存values) + if (!a.scriptRes) return; + let id = ""; + let d: Deferred | null = null; + if (!a.valueDaoUpdatetime) { + // 没有 setValues 直接 listValues / getValues 的话, valueDaoUpdatetime 为 undefined + // 要向 service_worker 发出请求,更新 页面缓存values,并触发 valueDaoUpdatetime 设置。 + id = generateValChangeId(); + d = deferred(); + valueChangePromiseMap.set(id, d.resolve); // 在 valueUpdate 里放行 Promise + } + const updatetimePromise = a.sendMessage("internalApiWaitForFreshValueState", [id]); + // 这里返回的 updatetime 是现时最新的 updatetime + const updatetime = (await Promise.all([updatetimePromise, d?.promise]))[0]; + if (updatetime && a.valueDaoUpdatetime && a.valueDaoUpdatetime < updatetime) { + // 未同步至最新状态,先等待 + // 由于 internalApiWaitForFreshValueState 返回的 updatetime 较新 + // 期待 pushToTab -> valueUpdate 的触发 + // 只要有 >=updatetime 的 valueUpdate, 就可以放行 + const readFreshes = (a.readFreshes ||= new Map>()); + let d = readFreshes.get(updatetime); + if (!d) { + readFreshes.set(updatetime, (d = deferred())); + } + await d.promise; + } + // valueDaoUpdatetime 最新,表示现在 缓存values 也是最新。可进行 listValues, getValues 等操作 + } + static _GM_getValue(a: GMApi, key: string, defaultValue?: any) { if (!a.scriptRes) return undefined; const ret = a.scriptRes.value[key]; @@ -251,50 +349,46 @@ export default class GMApi extends GM_Base { @GMContext.API() public ["GM.getValue"](key: string, defaultValue?: any): Promise { // 兼容GM.getValue - return new Promise((resolve) => { - const ret = _GM_getValue(this, key, defaultValue); - resolve(ret); + return waitForFreshValueState(this).then(() => { + return _GM_getValue(this, key, defaultValue); }); } - 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进行一次转化 @@ -349,10 +443,10 @@ export default class GMApi extends GM_Base { @GMContext.API() public ["GM.listValues"](): Promise { // Asynchronous wrapper for GM_listValues to support GM.listValues - return new Promise((resolve) => { - if (!this.scriptRes) return resolve([]); + return waitForFreshValueState(this).then(() => { + if (!this.scriptRes) return []; const keys = Object.keys(this.scriptRes.value); - resolve(keys); + return keys; }); } @@ -396,9 +490,8 @@ export default class GMApi extends GM_Base { @GMContext.API({ depend: ["GM_getValues"] }) public ["GM.getValues"](keysOrDefaults: TGMKeyValue | string[] | null | undefined): Promise { if (!this.scriptRes) return new Promise(() => {}); - return new Promise((resolve) => { - const ret = this.GM_getValues(keysOrDefaults); - resolve(ret); + return waitForFreshValueState(this).then(() => { + return this.GM_getValues(keysOrDefaults); }); } @@ -1365,4 +1458,4 @@ export default class GMApi extends GM_Base { export const { createGMBase } = GM_Base; // 从 GMApi 对象中解构出内部函数,用于后续本地使用,不导出 -const { _GM_getValue, _GM_cookie, _GM_setValue, _GM_setValues, _GM_download } = GMApi; +const { waitForFreshValueState, _GM_getValue, _GM_cookie, _GM_setValue, _GM_setValues, _GM_download } = GMApi; diff --git a/src/app/service/content/inject.ts b/src/app/service/content/inject.ts index 7fa8d2d8b..d7aaffe17 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 98a52dd70..22fad1b4b 100644 --- a/src/app/service/content/script_executor.ts +++ b/src/app/service/content/script_executor.ts @@ -2,7 +2,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 { addStyle, definePropertyListener } from "./utils"; import type { TScriptInfo } from "@App/app/repo/scripts"; import { DefinedFlags } from "../service_worker/runtime.consts"; @@ -35,11 +35,13 @@ export class ScriptExecutor { } } - 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 { data, storageName } = sendData; + for (const [uuid, list] of Object.entries(data)) { + for (const val of this.execMap.values()) { + if (val.scriptRes.uuid === uuid || getStorageName(val.scriptRes) === storageName) { + val.valueUpdate(storageName, uuid, list); + } } } } diff --git a/src/app/service/content/types.ts b/src/app/service/content/types.ts index 6f76847b8..dce3f3cbe 100644 --- a/src/app/service/content/types.ts +++ b/src/app/service/content/types.ts @@ -29,7 +29,12 @@ export type ValueUpdateDataEncoded = { uuid: string; storageName: string; // 储存name sender: ValueUpdateSender; - valueUpdated: boolean; + updatetime: number; +}; + +export type ValueUpdateSendData = { + storageName: string; + data: 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..7a687b24c 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"; @@ -323,20 +323,25 @@ 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); - } - }); - // 更新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; + valueUpdate(sendData: ValueUpdateSendData) { + const storageName = sendData.storageName; + for (const [uuid, list] of Object.entries(sendData.data)) { + // 转发给脚本 + this.execScripts.forEach((val) => { + if (val.scriptRes.uuid === uuid || getStorageName(val.scriptRes) === storageName) { + val.valueUpdate(storageName, uuid, list); + } + }); + for (const data of list) { + const dataEntries = data.entries; + // 更新crontabScripts中的脚本值 + for (const script of this.crontabSripts) { + if (script.uuid === uuid || getStorageName(script) === storageName) { + for (const [key, rTyped1, _rTyped2] of dataEntries) { + const value = decodeRValue(rTyped1); + script.value[key] = value; + } + } } } } 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 ca5d65eb8..89cb8d9e6 100644 --- a/src/app/service/service_worker/gm_api/gm_api.ts +++ b/src/app/service/service_worker/gm_api/gm_api.ts @@ -411,6 +411,23 @@ export default class GMApi { return true; } + @PermissionVerify.API({ + default: true, + }) + async internalApiWaitForFreshValueState(request: GMApiRequest<[string]>, sender: IGetSender) { + const param = request.params; + if (param.length !== 1) { + throw new Error("there must be one parameter"); + } + const id = param[0]; + const valueSender = { + runFlag: request.runFlag, + tabId: sender.getSender()?.tab?.id || -1, + }; + const ret = await this.value.waitForFreshValueState(request.script.uuid, id, valueSender); + return ret; + } + @PermissionVerify.API({ link: ["GM_deleteValue", "GM_deleteValues"] }) async GM_setValue(request: GMApiRequest<[string, string, any?]>, sender: IGetSender) { if (!request.params || request.params.length < 2) { @@ -425,7 +442,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 579c7679d..e1530956d 100644 --- a/src/app/service/service_worker/runtime.ts +++ b/src/app/service/service_worker/runtime.ts @@ -465,15 +465,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..39c18c3e4 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); @@ -121,20 +141,25 @@ describe("ValueService - setValue 方法测试", () => { expect(valueService.pushValueToTab).toHaveBeenCalledTimes(1); expect(valueService.pushValueToTab).toHaveBeenNthCalledWith( 1, + getStorageName(mockScript), 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, + [mockScript.uuid]: [ + expect.objectContaining({ + entries: 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 +191,7 @@ describe("ValueService - setValue 方法测试", () => { valueSender: mockSender, isReplace: false, }); + await flush(); // 验证结果 expect(mockScriptDAO.get).toHaveBeenCalledWith(mockScript.uuid); @@ -174,20 +200,24 @@ describe("ValueService - setValue 方法测试", () => { expect(valueService.pushValueToTab).toHaveBeenCalledTimes(1); expect(valueService.pushValueToTab).toHaveBeenNthCalledWith( 1, + getStorageName(mockScript), 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, + [mockScript.uuid]: [ + expect.objectContaining({ + entries: 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 +258,7 @@ describe("ValueService - setValue 方法测试", () => { valueSender: mockSender, isReplace: false, }); + await flush(); // 验证结果 expect(mockScriptDAO.get).toHaveBeenCalledWith(mockScript.uuid); @@ -236,23 +267,24 @@ describe("ValueService - setValue 方法测试", () => { expect(valueService.pushValueToTab).toHaveBeenCalledTimes(1); expect(valueService.pushValueToTab).toHaveBeenNthCalledWith( 1, + getStorageName(mockScript), 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, + [mockScript.uuid]: [ + expect.objectContaining({ + entries: 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 +321,7 @@ describe("ValueService - setValue 方法测试", () => { valueSender: mockSender, isReplace: false, }); + await flush(); // 验证结果 - 不应该保存或发送更新 expect(mockScriptDAO.get).toHaveBeenCalledWith(mockScript.uuid); @@ -297,20 +330,24 @@ describe("ValueService - setValue 方法测试", () => { expect(valueService.pushValueToTab).toHaveBeenCalledTimes(1); expect(valueService.pushValueToTab).toHaveBeenNthCalledWith( 1, + getStorageName(mockScript), 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, + [mockScript.uuid]: [ + expect.objectContaining({ + entries: 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 +378,7 @@ describe("ValueService - setValue 方法测试", () => { valueSender: mockSender, isReplace: false, }); + await flush(); // 验证结果 expect(mockValueDAO.save).toHaveBeenCalled(); @@ -371,6 +409,7 @@ describe("ValueService - setValue 方法测试", () => { isReplace: false, }) ).rejects.toThrow("script not found"); + await flush(); // 验证不会执行后续操作 expect(mockValueDAO.get).not.toHaveBeenCalled(); @@ -396,10 +435,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 +457,50 @@ 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, + getStorageName(mockScript), 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, + [mockScript.uuid]: [ + expect.objectContaining({ + entries: 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({ + entries: 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, + }), + ], }) ); - 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), - }), - storageName: getStorageName(mockScript), - uuid: mockScript.uuid, - valueUpdated: true, - }) + expect(mockMessageQueue.emit).toHaveBeenCalledTimes(1); + expect(mockMessageQueue.emit).toHaveBeenNthCalledWith( + 1, + "valueUpdate", + expectedValueUpdateEventEmit(mockScript, 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 }); }); }); diff --git a/src/app/service/service_worker/value.ts b/src/app/service/service_worker/value.ts index 5e0f44cd1..c41df03cf 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 } 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; @@ -75,9 +93,41 @@ export class ValueService { return this.getScriptValueDetails(script).then((res) => res[0]); } + async waitForFreshValueState(uuid: string, id: string, valueSender: ValueUpdateSender): Promise { + if (id) { + await this.setValues({ uuid, id, keyValuePairs: [], valueSender, isReplace: false }); + } + // 查询出脚本 + const script = await this.scriptDAO.get(uuid); + if (!script) { + throw new Error("script not found"); + } + // 查询老的值 + const storageName = getStorageName(script); + // 使用事务来保证数据一致性 + const cacheKey = `${CACHE_KEY_SET_VALUE}${storageName}`; + const ret = await stackAsyncTask(cacheKey, async () => { + const valueModel: Value | undefined = await this.valueDAO.get(storageName); + // await this.valueDAO.save(storageName, valueModel); + return valueModel?.updatetime; + }); + return ret || 0; + } + // 推送值到tab - async pushValueToTab(sendData: T) { - const { storageName } = sendData; + async pushValueToTab>(storageName: string, data: T) { + const sendData: ValueUpdateSendData = { storageName, data }; + /* + --- data structure --- + { + storageName: XXXX + { + uuid1: data1 + uuid2: data2 + ... + } + } + */ chrome.tabs.query({}, (tabs) => { const lastError = chrome.runtime.lastError; if (lastError) { @@ -111,29 +161,25 @@ 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; + let valueModel: Value | undefined = await this.valueDAO.get(storageName); + const taskList = taskListRef.slice(0); + taskListRef.length = 0; + valueUpdateTasks.delete(storageName); + // ------ 读取 & 更新 ------ + let updatetime = 0; + const listRetToTab: Record = {}; + let valueModelUpdated = false; + let hasValueUpdated = false; + for (const task of taskList) { + const entries = [] as ValueUpdateDataREntry[]; + const { uuid, keyValuePairs, isReplace, ts } = task; + let oldValueRecord: { [key: string]: any } = {}; + const now = aNow(); // 保证严格递增 + let newData; if (!valueModel) { - const now = Date.now(); const dataModel: { [key: string]: any } = {}; for (const [key, rTyped1] of keyValuePairs) { const value = decodeRValue(rTyped1); @@ -151,10 +197,11 @@ export class ValueService { createtime: ts ? Math.min(ts, now) : now, updatetime: ts ? Math.min(ts, now) : now, }; + newData = dataModel; } else { let changed = false; - let dataModel = (oldValueRecord = valueModel.data); - dataModel = { ...dataModel }; // 每次储存使用新参考 + oldValueRecord = valueModel.data; + const dataModel = { ...oldValueRecord }; // 每次储存使用新参考 const containedKeys = new Set(); for (const [key, rTyped1] of keyValuePairs) { containedKeys.add(key); @@ -182,24 +229,97 @@ export class ValueService { } } } - if (!changed) return false; - valueModel.data = dataModel; // 每次储存使用新参考 + if (changed) { + newData = dataModel; + } } - await this.valueDAO.save(storageName, valueModel); - return true; - }); + if (newData) { + valueModel.updatetime = now; + valueModel.data = newData; // 每次储存使用新参考 + valueModelUpdated = true; + } + updatetime = valueModel.updatetime; + + { + const { uuid, id, valueSender } = task; + let list = listRetToTab[uuid]; + if (!list) { + listRetToTab[uuid] = list = []; + } + const valueUpdated = entries.length > 0; + if (valueUpdated) hasValueUpdated = true; + list.push({ + id, + entries: entries, + uuid, + storageName, + sender: valueSender, + valueUpdated, + 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, listRetToTab); + // 针对各脚本,只需要发送一次最后的结果 + const valueUpdateEmits = new Map(); + for (const task of taskList) { + const { uuid, status, isEarlyStart } = task; + valueUpdateEmits.set(uuid, { status, isEarlyStart }); + } + 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) {