Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 76 additions & 19 deletions src/app/service/content/gm_api.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { describe, expect, it, vi } from "vitest";
import { describe, expect, it, vi, type Mock } from "vitest";
import ExecScript from "./exec_script";
import type { ScriptLoadInfo } from "../service_worker/types";
import type { GMInfoEnv, ScriptFunc } from "./types";
Expand Down Expand Up @@ -107,6 +107,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) => {
const p1 = entry?.[0]?.data?.params[1];
return typeof p1 === "string" && p1?.match(/__forceUpdateTimeRefresh::0\.\d+__/);
});
if (forceUpdateTimeRefreshIdx >= 0) {
const actualCall = mockSendMessage.mock.calls[forceUpdateTimeRefreshIdx][0];
expect(mockSendMessage).toHaveBeenNthCalledWith(
forceUpdateTimeRefreshIdx + 1,
expect.objectContaining({
action: "content/runtime/gmApi",
data: {
api: "GM_setValue",
params: [expect.stringMatching(/^.+::\d+$/), expect.stringMatching(/__forceUpdateTimeRefresh::0\.\d+__/)],
runFlag: expect.any(String),
uuid: undefined,
},
})
);
exec.valueUpdate({
id: actualCall.data.params[0],
entries: encodeMessage([[actualCall.data.params[1], undefined, undefined]]),
uuid: script.uuid,
storageName: script.uuid,
sender: { runFlag: exec.sandboxContext!.runFlag, tabId: -2 },
valueUpdated: false,
updatetime: Date.now(),
});
}
};
it.concurrent("GM_getValue", async () => {
const script = Object.assign({}, scriptRes) as ScriptLoadInfo;
script.value = { test: "ok" };
Expand All @@ -123,10 +157,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!");
});

Expand Down Expand Up @@ -163,10 +202,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");
});

Expand All @@ -179,10 +223,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();
const retPromise = exec.exec();
valueDaoUpdatetimeFix(mockSendMessage, exec, script);
const ret = await retPromise;
expect(ret).toEqual("test5-test2-test3-test1"); // TM也沒有sort
});

Expand Down Expand Up @@ -212,10 +261,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");
Expand Down Expand Up @@ -493,7 +547,7 @@ describe.concurrent("GM_value", () => {
api: "GM_setValues",
params: [
// event id
expect.stringMatching(/^.+::\d$/),
expect.stringMatching(/^.+::\d+$/),
// the object payload
expect.objectContaining({
k: expect.stringMatching(/^##[\d.]+##$/),
Expand All @@ -519,7 +573,7 @@ describe.concurrent("GM_value", () => {
api: "GM_setValues",
params: [
// event id
expect.stringMatching(/^.+::\d$/),
expect.stringMatching(/^.+::\d+$/),
// the object payload
expect.objectContaining({
k: expect.stringMatching(/^##[\d.]+##$/),
Expand Down Expand Up @@ -570,7 +624,7 @@ describe.concurrent("GM_value", () => {
api: "GM_setValues",
params: [
// event id
expect.stringMatching(/^.+::\d$/),
expect.stringMatching(/^.+::\d+$/),
// the object payload
expect.objectContaining({
k: expect.stringMatching(/^##[\d.]+##$/),
Expand All @@ -596,7 +650,7 @@ describe.concurrent("GM_value", () => {
api: "GM_setValue",
params: [
// event id
expect.stringMatching(/^.+::\d$/),
expect.stringMatching(/^.+::\d+$/),
// the string payload
"b",
],
Expand Down Expand Up @@ -641,7 +695,7 @@ describe.concurrent("GM_value", () => {
api: "GM_setValues",
params: [
// event id
expect.stringMatching(/^.+::\d$/),
expect.stringMatching(/^.+::\d+$/),
// the object payload
expect.objectContaining({
k: expect.stringMatching(/^##[\d.]+##$/),
Expand All @@ -667,7 +721,7 @@ describe.concurrent("GM_value", () => {
api: "GM_setValues",
params: [
// event id
expect.stringMatching(/^.+::\d$/),
expect.stringMatching(/^.+::\d+$/),
// the string payload
expect.objectContaining({
k: expect.stringMatching(/^##[\d.]+##$/),
Expand Down Expand Up @@ -715,6 +769,7 @@ describe.concurrent("GM_value", () => {
storageName: script.uuid,
sender: { runFlag: exec.sandboxContext!.runFlag, tabId: -2 },
valueUpdated: true,
updatetime: Date.now(),
});
const ret = await retPromise;
expect(ret).toEqual({ name: "param1", oldValue: undefined, newValue: 123, remote: false });
Expand Down Expand Up @@ -750,6 +805,7 @@ describe.concurrent("GM_value", () => {
storageName: "testStorage",
sender: { runFlag: "user", tabId: -2 },
valueUpdated: true,
updatetime: Date.now(),
});
const ret2 = await retPromise;
expect(ret2).toEqual({ name: "param2", oldValue: undefined, newValue: 456, remote: true });
Expand Down Expand Up @@ -785,6 +841,7 @@ describe.concurrent("GM_value", () => {
storageName: script.uuid,
sender: { runFlag: exec.sandboxContext!.runFlag, tabId: -2 },
valueUpdated: true,
updatetime: Date.now(),
});

const ret = await retPromise;
Expand Down
67 changes: 53 additions & 14 deletions src/app/service/content/gm_api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,12 @@ class GM_Base implements IGM_Base {
@GMContext.protected()
public setInvalidContext!: () => void;

@GMContext.protected()
public readFreshes: Set<{ updatetime: number; resolveFn: any }> | undefined;

@GMContext.protected()
valueDaoUpdatetime: number | undefined;

// 单次回调使用
@GMContext.protected()
public async sendMessage(api: string, params: any[]) {
Expand Down Expand Up @@ -143,9 +149,9 @@ class GM_Base implements IGM_Base {

@GMContext.protected()
public valueUpdate(data: ValueUpdateDataEncoded) {
if (!this.scriptRes || !this.valueChangeListener) return;
if (!this.scriptRes) return;
const scriptRes = this.scriptRes;
const { id, uuid, entries, storageName, sender, valueUpdated } = data;
const { id, uuid, entries, storageName, sender, valueUpdated, updatetime } = data;
if (uuid === scriptRes.uuid || storageName === getStorageName(scriptRes)) {
const valueStore = scriptRes.value;
const remote = sender.runFlag !== this.runFlag;
Expand All @@ -167,9 +173,21 @@ class GM_Base implements IGM_Base {
} else {
valueStore[key] = value;
}
this.valueChangeListener.execute(key, oldValue, value, remote, sender.tabId);
this.valueChangeListener?.execute(key, oldValue, value, remote, sender.tabId);
}
}
if (updatetime) {
const readFreshes = this.readFreshes;
if (readFreshes) {
for (const entry of readFreshes) {
if (updatetime >= entry.updatetime) {
readFreshes.delete(entry);
entry.resolveFn();
}
}
}
this.valueDaoUpdatetime = updatetime;
}
}
}

Expand Down Expand Up @@ -224,6 +242,29 @@ export default class GMApi extends GM_Base {
);
}

static async waitForFreshValueState(a: GMApi): Promise<void> {
// 读取前没有任何 valueUpdate 的话,valueDaoUpdatetime 为 undefined
// valueDaoUpdatetime 需透过 valueUpdate 提供
if (!a.valueDaoUpdatetime) {
const keyName = `__forceUpdateTimeRefresh::${Math.random()}__`;
// 删一个不存在的 key 触发 valueDaoUpdatetime 设置
await new Promise((resolve) => {
_GM_setValue(a, resolve, keyName, undefined);
});
}
const updatetime = await a.sendMessage("GM_waitForFreshValueState", [true]);
if (updatetime && a.valueDaoUpdatetime && a.valueDaoUpdatetime < updatetime) {
// 未同步至最新状态,先等待
return new Promise((resolve) => {
a.readFreshes ||= new Set();
a.readFreshes.add({
updatetime: updatetime,
resolveFn: resolve,
});
});
}
}

static _GM_getValue(a: GMApi, key: string, defaultValue?: any) {
if (!a.scriptRes) return undefined;
const ret = a.scriptRes.value[key];
Expand All @@ -242,9 +283,8 @@ export default class GMApi extends GM_Base {
@GMContext.API()
public ["GM.getValue"](key: string, defaultValue?: any): Promise<any> {
// 兼容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);
});
}

Expand Down Expand Up @@ -339,10 +379,10 @@ export default class GMApi extends GM_Base {
@GMContext.API()
public ["GM.listValues"](): Promise<string[]> {
// 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;
});
}

Expand Down Expand Up @@ -386,9 +426,8 @@ export default class GMApi extends GM_Base {
@GMContext.API({ depend: ["GM_getValues"] })
public ["GM.getValues"](keysOrDefaults: TGMKeyValue | string[] | null | undefined): Promise<TGMKeyValue> {
if (!this.scriptRes) return new Promise<TGMKeyValue>(() => {});
return new Promise((resolve) => {
const ret = this.GM_getValues(keysOrDefaults);
resolve(ret);
return waitForFreshValueState(this).then(() => {
return this.GM_getValues(keysOrDefaults);
});
}

Expand Down Expand Up @@ -574,7 +613,7 @@ export default class GMApi extends GM_Base {
// 每个 contentEnvKey(执行环境)初始化时会重设;不持久化、只保证当前环境内递增唯一。
menuIdCounter: number | undefined;

// 菜单注冊累计器 - 用於穩定同一Tab不同frame之選項的單獨項目不合併狀態
// 菜单注册累计器 - 用于稳定同一Tab不同frame之选项的单独项目不合并状态
// 每个 contentEnvKey(执行环境)初始化时会重设;不持久化、只保证当前环境内递增唯一。
regMenuCounter: number | undefined;

Expand Down Expand Up @@ -1420,4 +1459,4 @@ export default class GMApi extends GM_Base {
export const { createGMBase } = GM_Base;

// 从 GMApi 对象中解构出内部函数,用于后续本地使用,不导出
const { _GM_getValue, _GM_cookie, _GM_setValue, _GM_setValues, _GM_xmlhttpRequest } = GMApi;
const { waitForFreshValueState, _GM_getValue, _GM_cookie, _GM_setValue, _GM_setValues, _GM_xmlhttpRequest } = GMApi;
1 change: 1 addition & 0 deletions src/app/service/content/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export type ValueUpdateDataEncoded = {
storageName: string; // 储存name
sender: ValueUpdateSender;
valueUpdated: boolean;
updatetime: number;
};

// gm_api.ts
Expand Down
15 changes: 15 additions & 0 deletions src/app/service/service_worker/gm_api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,21 @@ export default class GMApi {
return true;
}

@PermissionVerify.API({
default: true,
})
async GM_waitForFreshValueState(request: GMApiRequest<[boolean]>, sender: IGetSender) {
const param = request.params;
if (param.length !== 1) {
throw new Error("there must be one parameter");
}
const ret = await this.value.waitForFreshValueState(request.script.uuid, {
runFlag: request.runFlag,
tabId: sender.getSender()?.tab?.id || -1,
});
return ret;
}
Copy link

Copilot AI Nov 8, 2025

Choose a reason for hiding this comment

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

GM_waitForFreshValueState 函数接收的参数声明为 GMApiRequest<[boolean]>,并验证参数长度必须为 1,但实际上并未使用该参数。在 content/gm_api.ts:255 调用时传递的是 [true],但在函数内部完全没有使用 request.params[0]。要么应该使用该参数,要么应该将类型改为 GMApiRequest<[]> 并移除长度验证。

Copilot uses AI. Check for mistakes.
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

之前好像不放东西的话,传送有问题。我没有看到其他没用param的API。
所以就放一个 true 了
先这样吧


@PermissionVerify.API({ link: ["GM_deleteValue"] })
async GM_setValue(request: GMApiRequest<[string, string, any?]>, sender: IGetSender) {
if (!request.params || request.params.length < 2) {
Expand Down
Loading
Loading