Skip to content

Commit 04d6641

Browse files
committed
异歩 GM.getValue/getValues/listValues 修正
1 parent 05a7e7e commit 04d6641

File tree

5 files changed

+180
-38
lines changed

5 files changed

+180
-38
lines changed

src/app/service/content/gm_api.test.ts

Lines changed: 76 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { describe, expect, it, vi } from "vitest";
1+
import { describe, expect, it, vi, type Mock } from "vitest";
22
import ExecScript from "./exec_script";
33
import type { ScriptLoadInfo } from "../service_worker/types";
44
import type { GMInfoEnv, ScriptFunc } from "./types";
@@ -107,6 +107,40 @@ describe.concurrent("window.*", () => {
107107
});
108108

109109
describe.concurrent("GM Api", () => {
110+
const valueDaoUpdatetimeFix = (
111+
mockSendMessage: Mock<(...args: any[]) => any>,
112+
exec: ExecScript,
113+
script: ScriptLoadInfo
114+
) => {
115+
const forceUpdateTimeRefreshIdx = mockSendMessage.mock.calls.findIndex((entry) => {
116+
const p1 = entry?.[0]?.data?.params[1];
117+
return typeof p1 === "string" && p1?.match(/__forceUpdateTimeRefresh::0\.\d+__/);
118+
});
119+
if (forceUpdateTimeRefreshIdx >= 0) {
120+
const actualCall = mockSendMessage.mock.calls[forceUpdateTimeRefreshIdx][0];
121+
expect(mockSendMessage).toHaveBeenNthCalledWith(
122+
forceUpdateTimeRefreshIdx + 1,
123+
expect.objectContaining({
124+
action: "content/runtime/gmApi",
125+
data: {
126+
api: "GM_setValue",
127+
params: [expect.stringMatching(/^.+::\d+$/), expect.stringMatching(/__forceUpdateTimeRefresh::0\.\d+__/)],
128+
runFlag: expect.any(String),
129+
uuid: undefined,
130+
},
131+
})
132+
);
133+
exec.valueUpdate({
134+
id: actualCall.data.params[0],
135+
entries: encodeMessage([[actualCall.data.params[1], undefined, undefined]]),
136+
uuid: script.uuid,
137+
storageName: script.uuid,
138+
sender: { runFlag: exec.sandboxContext!.runFlag, tabId: -2 },
139+
valueUpdated: false,
140+
updatetime: Date.now(),
141+
});
142+
}
143+
};
110144
it.concurrent("GM_getValue", async () => {
111145
const script = Object.assign({}, scriptRes) as ScriptLoadInfo;
112146
script.value = { test: "ok" };
@@ -123,10 +157,15 @@ describe.concurrent("GM Api", () => {
123157
script.value = { test: "ok" };
124158
script.metadata.grant = ["GM.getValue"];
125159
script.code = `return GM.getValue("test").then(v=>v+"!");`;
126-
// @ts-ignore
127-
const exec = new ExecScript(script, undefined, undefined, nilFn, envInfo);
160+
const mockSendMessage = vi.fn().mockResolvedValue({ code: 0 });
161+
const mockMessage = {
162+
sendMessage: mockSendMessage,
163+
} as unknown as Message;
164+
const exec = new ExecScript(script, "content", mockMessage, nilFn, envInfo);
128165
exec.scriptFunc = compileScript(compileScriptCode(script));
129-
const ret = await exec.exec();
166+
const retPromise = exec.exec();
167+
valueDaoUpdatetimeFix(mockSendMessage, exec, script);
168+
const ret = await retPromise;
130169
expect(ret).toEqual("ok!");
131170
});
132171

@@ -163,10 +202,15 @@ describe.concurrent("GM Api", () => {
163202
script.value = { test1: "23", test2: "45", test3: "67" };
164203
script.metadata.grant = ["GM.listValues"];
165204
script.code = `return GM.listValues().then(v=>v.join("-"));`;
166-
// @ts-ignore
167-
const exec = new ExecScript(script, undefined, undefined, nilFn, envInfo);
205+
const mockSendMessage = vi.fn().mockResolvedValue({ code: 0 });
206+
const mockMessage = {
207+
sendMessage: mockSendMessage,
208+
} as unknown as Message;
209+
const exec = new ExecScript(script, "content", mockMessage, nilFn, envInfo);
168210
exec.scriptFunc = compileScript(compileScriptCode(script));
169-
const ret = await exec.exec();
211+
const retPromise = exec.exec();
212+
valueDaoUpdatetimeFix(mockSendMessage, exec, script);
213+
const ret = await retPromise;
170214
expect(ret).toEqual("test1-test2-test3");
171215
});
172216

@@ -179,10 +223,15 @@ describe.concurrent("GM Api", () => {
179223
script.value.test1 = "40";
180224
script.metadata.grant = ["GM.listValues"];
181225
script.code = `return GM.listValues().then(v=>v.join("-"));`;
182-
// @ts-ignore
183-
const exec = new ExecScript(script, undefined, undefined, nilFn, envInfo);
226+
const mockSendMessage = vi.fn().mockResolvedValue({ code: 0 });
227+
const mockMessage = {
228+
sendMessage: mockSendMessage,
229+
} as unknown as Message;
230+
const exec = new ExecScript(script, "content", mockMessage, nilFn, envInfo);
184231
exec.scriptFunc = compileScript(compileScriptCode(script));
185-
const ret = await exec.exec();
232+
const retPromise = exec.exec();
233+
valueDaoUpdatetimeFix(mockSendMessage, exec, script);
234+
const ret = await retPromise;
186235
expect(ret).toEqual("test5-test2-test3-test1"); // TM也沒有sort
187236
});
188237

@@ -212,10 +261,15 @@ describe.concurrent("GM Api", () => {
212261
script.value = { test1: "23", test2: 45, test3: "67" };
213262
script.metadata.grant = ["GM.getValues"];
214263
script.code = `return GM.getValues(["test2", "test3", "test1"]).then(v=>v);`;
215-
// @ts-ignore
216-
const exec = new ExecScript(script, undefined, undefined, nilFn, envInfo);
264+
const mockSendMessage = vi.fn().mockResolvedValue({ code: 0 });
265+
const mockMessage = {
266+
sendMessage: mockSendMessage,
267+
} as unknown as Message;
268+
const exec = new ExecScript(script, "content", mockMessage, nilFn, envInfo);
217269
exec.scriptFunc = compileScript(compileScriptCode(script));
218-
const ret = await exec.exec();
270+
const retPromise = exec.exec();
271+
valueDaoUpdatetimeFix(mockSendMessage, exec, script);
272+
const ret = await retPromise;
219273
expect(ret.test1).toEqual("23");
220274
expect(ret.test2).toEqual(45);
221275
expect(ret.test3).toEqual("67");
@@ -493,7 +547,7 @@ describe.concurrent("GM_value", () => {
493547
api: "GM_setValues",
494548
params: [
495549
// event id
496-
expect.stringMatching(/^.+::\d$/),
550+
expect.stringMatching(/^.+::\d+$/),
497551
// the object payload
498552
expect.objectContaining({
499553
k: expect.stringMatching(/^##[\d.]+##$/),
@@ -519,7 +573,7 @@ describe.concurrent("GM_value", () => {
519573
api: "GM_setValues",
520574
params: [
521575
// event id
522-
expect.stringMatching(/^.+::\d$/),
576+
expect.stringMatching(/^.+::\d+$/),
523577
// the object payload
524578
expect.objectContaining({
525579
k: expect.stringMatching(/^##[\d.]+##$/),
@@ -570,7 +624,7 @@ describe.concurrent("GM_value", () => {
570624
api: "GM_setValues",
571625
params: [
572626
// event id
573-
expect.stringMatching(/^.+::\d$/),
627+
expect.stringMatching(/^.+::\d+$/),
574628
// the object payload
575629
expect.objectContaining({
576630
k: expect.stringMatching(/^##[\d.]+##$/),
@@ -596,7 +650,7 @@ describe.concurrent("GM_value", () => {
596650
api: "GM_setValue",
597651
params: [
598652
// event id
599-
expect.stringMatching(/^.+::\d$/),
653+
expect.stringMatching(/^.+::\d+$/),
600654
// the string payload
601655
"b",
602656
],
@@ -641,7 +695,7 @@ describe.concurrent("GM_value", () => {
641695
api: "GM_setValues",
642696
params: [
643697
// event id
644-
expect.stringMatching(/^.+::\d$/),
698+
expect.stringMatching(/^.+::\d+$/),
645699
// the object payload
646700
expect.objectContaining({
647701
k: expect.stringMatching(/^##[\d.]+##$/),
@@ -667,7 +721,7 @@ describe.concurrent("GM_value", () => {
667721
api: "GM_setValues",
668722
params: [
669723
// event id
670-
expect.stringMatching(/^.+::\d$/),
724+
expect.stringMatching(/^.+::\d+$/),
671725
// the string payload
672726
expect.objectContaining({
673727
k: expect.stringMatching(/^##[\d.]+##$/),
@@ -715,6 +769,7 @@ describe.concurrent("GM_value", () => {
715769
storageName: script.uuid,
716770
sender: { runFlag: exec.sandboxContext!.runFlag, tabId: -2 },
717771
valueUpdated: true,
772+
updatetime: Date.now(),
718773
});
719774
const ret = await retPromise;
720775
expect(ret).toEqual({ name: "param1", oldValue: undefined, newValue: 123, remote: false });
@@ -750,6 +805,7 @@ describe.concurrent("GM_value", () => {
750805
storageName: "testStorage",
751806
sender: { runFlag: "user", tabId: -2 },
752807
valueUpdated: true,
808+
updatetime: Date.now(),
753809
});
754810
const ret2 = await retPromise;
755811
expect(ret2).toEqual({ name: "param2", oldValue: undefined, newValue: 456, remote: true });
@@ -785,6 +841,7 @@ describe.concurrent("GM_value", () => {
785841
storageName: script.uuid,
786842
sender: { runFlag: exec.sandboxContext!.runFlag, tabId: -2 },
787843
valueUpdated: true,
844+
updatetime: Date.now(),
788845
});
789846

790847
const ret = await retPromise;

src/app/service/content/gm_api.ts

Lines changed: 53 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,12 @@ class GM_Base implements IGM_Base {
103103
@GMContext.protected()
104104
public setInvalidContext!: () => void;
105105

106+
@GMContext.protected()
107+
public readFreshes: Set<{ updatetime: number; resolveFn: any }> | undefined;
108+
109+
@GMContext.protected()
110+
valueDaoUpdatetime: number | undefined;
111+
106112
// 单次回调使用
107113
@GMContext.protected()
108114
public async sendMessage(api: string, params: any[]) {
@@ -143,9 +149,9 @@ class GM_Base implements IGM_Base {
143149

144150
@GMContext.protected()
145151
public valueUpdate(data: ValueUpdateDataEncoded) {
146-
if (!this.scriptRes || !this.valueChangeListener) return;
152+
if (!this.scriptRes) return;
147153
const scriptRes = this.scriptRes;
148-
const { id, uuid, entries, storageName, sender, valueUpdated } = data;
154+
const { id, uuid, entries, storageName, sender, valueUpdated, updatetime } = data;
149155
if (uuid === scriptRes.uuid || storageName === getStorageName(scriptRes)) {
150156
const valueStore = scriptRes.value;
151157
const remote = sender.runFlag !== this.runFlag;
@@ -167,9 +173,21 @@ class GM_Base implements IGM_Base {
167173
} else {
168174
valueStore[key] = value;
169175
}
170-
this.valueChangeListener.execute(key, oldValue, value, remote, sender.tabId);
176+
this.valueChangeListener?.execute(key, oldValue, value, remote, sender.tabId);
171177
}
172178
}
179+
if (updatetime) {
180+
const readFreshes = this.readFreshes;
181+
if (readFreshes) {
182+
for (const entry of readFreshes) {
183+
if (updatetime >= entry.updatetime) {
184+
readFreshes.delete(entry);
185+
entry.resolveFn();
186+
}
187+
}
188+
}
189+
this.valueDaoUpdatetime = updatetime;
190+
}
173191
}
174192
}
175193

@@ -224,6 +242,29 @@ export default class GMApi extends GM_Base {
224242
);
225243
}
226244

245+
static async waitForFreshValueState(a: GMApi): Promise<void> {
246+
// 读取前没有任何 valueUpdate 的话,valueDaoUpdatetime 为 undefined
247+
// valueDaoUpdatetime 需透过 valueUpdate 提供
248+
if (!a.valueDaoUpdatetime) {
249+
const keyName = `__forceUpdateTimeRefresh::${Math.random()}__`;
250+
// 删一个不存在的 key 触发 valueDaoUpdatetime 设置
251+
await new Promise((resolve) => {
252+
_GM_setValue(a, resolve, keyName, undefined);
253+
});
254+
}
255+
const updatetime = await a.sendMessage("GM_waitForFreshValueState", [true]);
256+
if (updatetime && a.valueDaoUpdatetime && a.valueDaoUpdatetime < updatetime) {
257+
// 未同步至最新状态,先等待
258+
return new Promise((resolve) => {
259+
a.readFreshes ||= new Set();
260+
a.readFreshes.add({
261+
updatetime: updatetime,
262+
resolveFn: resolve,
263+
});
264+
});
265+
}
266+
}
267+
227268
static _GM_getValue(a: GMApi, key: string, defaultValue?: any) {
228269
if (!a.scriptRes) return undefined;
229270
const ret = a.scriptRes.value[key];
@@ -242,9 +283,8 @@ export default class GMApi extends GM_Base {
242283
@GMContext.API()
243284
public ["GM.getValue"](key: string, defaultValue?: any): Promise<any> {
244285
// 兼容GM.getValue
245-
return new Promise((resolve) => {
246-
const ret = _GM_getValue(this, key, defaultValue);
247-
resolve(ret);
286+
return waitForFreshValueState(this).then(() => {
287+
return _GM_getValue(this, key, defaultValue);
248288
});
249289
}
250290

@@ -339,10 +379,10 @@ export default class GMApi extends GM_Base {
339379
@GMContext.API()
340380
public ["GM.listValues"](): Promise<string[]> {
341381
// Asynchronous wrapper for GM_listValues to support GM.listValues
342-
return new Promise((resolve) => {
343-
if (!this.scriptRes) return resolve([]);
382+
return waitForFreshValueState(this).then(() => {
383+
if (!this.scriptRes) return [];
344384
const keys = Object.keys(this.scriptRes.value);
345-
resolve(keys);
385+
return keys;
346386
});
347387
}
348388

@@ -386,9 +426,8 @@ export default class GMApi extends GM_Base {
386426
@GMContext.API({ depend: ["GM_getValues"] })
387427
public ["GM.getValues"](keysOrDefaults: TGMKeyValue | string[] | null | undefined): Promise<TGMKeyValue> {
388428
if (!this.scriptRes) return new Promise<TGMKeyValue>(() => {});
389-
return new Promise((resolve) => {
390-
const ret = this.GM_getValues(keysOrDefaults);
391-
resolve(ret);
429+
return waitForFreshValueState(this).then(() => {
430+
return this.GM_getValues(keysOrDefaults);
392431
});
393432
}
394433

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

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

@@ -1420,4 +1459,4 @@ export default class GMApi extends GM_Base {
14201459
export const { createGMBase } = GM_Base;
14211460

14221461
// 从 GMApi 对象中解构出内部函数,用于后续本地使用,不导出
1423-
const { _GM_getValue, _GM_cookie, _GM_setValue, _GM_setValues, _GM_xmlhttpRequest } = GMApi;
1462+
const { waitForFreshValueState, _GM_getValue, _GM_cookie, _GM_setValue, _GM_setValues, _GM_xmlhttpRequest } = GMApi;

src/app/service/content/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export type ValueUpdateDataEncoded = {
3232
storageName: string; // 储存name
3333
sender: ValueUpdateSender;
3434
valueUpdated: boolean;
35+
updatetime: number;
3536
};
3637

3738
// gm_api.ts

src/app/service/service_worker/gm_api.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,21 @@ export default class GMApi {
331331
return true;
332332
}
333333

334+
@PermissionVerify.API({
335+
default: true,
336+
})
337+
async GM_waitForFreshValueState(request: GMApiRequest<[boolean]>, sender: IGetSender) {
338+
const param = request.params;
339+
if (param.length !== 1) {
340+
throw new Error("there must be one parameter");
341+
}
342+
const ret = await this.value.waitForFreshValueState(request.script.uuid, {
343+
runFlag: request.runFlag,
344+
tabId: sender.getSender()?.tab?.id || -1,
345+
});
346+
return ret;
347+
}
348+
334349
@PermissionVerify.API({ link: ["GM_deleteValue"] })
335350
async GM_setValue(request: GMApiRequest<[string, string, any?]>, sender: IGetSender) {
336351
if (!request.params || request.params.length < 2) {

0 commit comments

Comments
 (0)