From ac0c9536cbeebd69d0265836f93cdbd98e2fa740 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Tue, 11 Nov 2025 07:10:13 +0900 Subject: [PATCH 1/9] encode/decodeMessage -> encode/decodeRValue --- src/app/service/content/gm_api.test.ts | 78 +++-- src/app/service/content/gm_api.ts | 15 +- src/app/service/content/types.ts | 9 +- src/app/service/sandbox/runtime.ts | 9 +- src/app/service/service_worker/client.ts | 9 +- src/app/service/service_worker/gm_api.ts | 10 +- src/app/service/service_worker/value.ts | 51 ++-- src/pages/components/ScriptStorage/index.tsx | 10 +- .../components/UserConfigPanel/index.tsx | 6 +- src/pkg/utils/message_value.test.ts | 266 +++++++++++------- src/pkg/utils/message_value.ts | 148 +++++----- 11 files changed, 349 insertions(+), 262 deletions(-) diff --git a/src/app/service/content/gm_api.test.ts b/src/app/service/content/gm_api.test.ts index 05e8b8f24..28b4ddb3e 100644 --- a/src/app/service/content/gm_api.test.ts +++ b/src/app/service/content/gm_api.test.ts @@ -4,7 +4,7 @@ import type { ScriptLoadInfo } from "../service_worker/types"; import type { GMInfoEnv, ScriptFunc } from "./types"; import { compileScript, compileScriptCode } from "./utils"; import type { Message } from "@Packages/message/types"; -import { encodeMessage } from "@App/pkg/utils/message_value"; +import { encodeRValue } from "@App/pkg/utils/message_value"; import { v4 as uuidv4 } from "uuid"; const nilFn: ScriptFunc = () => {}; @@ -484,6 +484,12 @@ describe.concurrent("GM_value", () => { expect(mockSendMessage).toHaveBeenCalled(); expect(mockSendMessage).toHaveBeenCalledTimes(2); + const keyValuePairs1 = [ + ["a", encodeRValue(123)], + ["b", encodeRValue(456)], + ["c", encodeRValue("789")], + ]; + // 第一次调用:设置值为 123 expect(mockSendMessage).toHaveBeenNthCalledWith( 1, @@ -495,14 +501,7 @@ describe.concurrent("GM_value", () => { // event id expect.stringMatching(/^.+::\d$/), // the object payload - expect.objectContaining({ - k: expect.stringMatching(/^##[\d.]+##$/), - m: expect.objectContaining({ - a: 123, - b: 456, - c: "789", - }), - }), + keyValuePairs1, ], runFlag: expect.any(String), uuid: undefined, @@ -510,6 +509,11 @@ describe.concurrent("GM_value", () => { }) ); + const keyValuePairs2 = [ + ["a", encodeRValue(undefined)], + ["c", encodeRValue(undefined)], + ]; + // 第二次调用:删除值(设置为 undefined) expect(mockSendMessage).toHaveBeenNthCalledWith( 2, @@ -521,13 +525,7 @@ describe.concurrent("GM_value", () => { // event id expect.stringMatching(/^.+::\d$/), // the object payload - expect.objectContaining({ - k: expect.stringMatching(/^##[\d.]+##$/), - m: expect.objectContaining({ - a: expect.stringMatching(/^##[\d.]+##undefined$/), - c: expect.stringMatching(/^##[\d.]+##undefined$/), - }), - }), + keyValuePairs2, ], runFlag: expect.any(String), uuid: undefined, @@ -561,6 +559,11 @@ describe.concurrent("GM_value", () => { expect(mockSendMessage).toHaveBeenCalled(); expect(mockSendMessage).toHaveBeenCalledTimes(2); + const keyValuePairs1 = [ + ["a", encodeRValue(123)], + ["b", encodeRValue(456)], + ["c", encodeRValue("789")], + ]; // 第一次调用:设置值为 123 expect(mockSendMessage).toHaveBeenNthCalledWith( 1, @@ -572,14 +575,7 @@ describe.concurrent("GM_value", () => { // event id expect.stringMatching(/^.+::\d$/), // the object payload - expect.objectContaining({ - k: expect.stringMatching(/^##[\d.]+##$/), - m: expect.objectContaining({ - a: 123, - b: 456, - c: "789", - }), - }), + keyValuePairs1, ], runFlag: expect.any(String), uuid: undefined, @@ -632,6 +628,12 @@ describe.concurrent("GM_value", () => { expect(mockSendMessage).toHaveBeenCalled(); expect(mockSendMessage).toHaveBeenCalledTimes(2); + const keyValuePairs1 = [ + ["a", encodeRValue(123)], + ["b", encodeRValue(456)], + ["c", encodeRValue("789")], + ]; + // 第一次调用:设置值为 123 expect(mockSendMessage).toHaveBeenNthCalledWith( 1, @@ -643,14 +645,7 @@ describe.concurrent("GM_value", () => { // event id expect.stringMatching(/^.+::\d$/), // the object payload - expect.objectContaining({ - k: expect.stringMatching(/^##[\d.]+##$/), - m: expect.objectContaining({ - a: 123, - b: 456, - c: "789", - }), - }), + keyValuePairs1, ], runFlag: expect.any(String), uuid: undefined, @@ -658,6 +653,11 @@ describe.concurrent("GM_value", () => { }) ); + const keyValuePairs2 = [ + ["a", encodeRValue(undefined)], + ["c", encodeRValue(undefined)], + ]; + // 第二次调用:删除值(设置为 undefined) expect(mockSendMessage).toHaveBeenNthCalledWith( 2, @@ -669,13 +669,7 @@ describe.concurrent("GM_value", () => { // event id expect.stringMatching(/^.+::\d$/), // the string payload - expect.objectContaining({ - k: expect.stringMatching(/^##[\d.]+##$/), - m: expect.objectContaining({ - a: expect.stringMatching(/^##[\d.]+##undefined$/), - c: expect.stringMatching(/^##[\d.]+##undefined$/), - }), - }), + keyValuePairs2, ], runFlag: expect.any(String), uuid: undefined, @@ -710,7 +704,7 @@ describe.concurrent("GM_value", () => { // 模拟值变化 exec.valueUpdate({ id: "id-1", - entries: encodeMessage([["param1", 123, undefined]]), + entries: [["param1", encodeRValue(123), encodeRValue(undefined)]], uuid: script.uuid, storageName: script.uuid, sender: { runFlag: exec.sandboxContext!.runFlag, tabId: -2 }, @@ -745,7 +739,7 @@ describe.concurrent("GM_value", () => { // 模拟值变化 exec.valueUpdate({ id: "id-2", - entries: encodeMessage([["param2", 456, undefined]]), + entries: [["param2", encodeRValue(456), encodeRValue(undefined)]], uuid: script.uuid, storageName: "testStorage", sender: { runFlag: "user", tabId: -2 }, @@ -780,7 +774,7 @@ describe.concurrent("GM_value", () => { // 触发valueUpdate exec.valueUpdate({ id: id, - entries: encodeMessage([["a", 123, undefined]]), + entries: [["a", encodeRValue(123), encodeRValue(undefined)]], uuid: script.uuid, storageName: script.uuid, sender: { runFlag: exec.sandboxContext!.runFlag, tabId: -2 }, diff --git a/src/app/service/content/gm_api.ts b/src/app/service/content/gm_api.ts index 3eaf219f9..a630d96d8 100644 --- a/src/app/service/content/gm_api.ts +++ b/src/app/service/content/gm_api.ts @@ -19,7 +19,7 @@ import type { MessageRequest } from "../service_worker/types"; import { connect, sendMessage } from "@Packages/message/client"; import { getStorageName } from "@App/pkg/utils/utils"; import { ListenerManager } from "./listener_manager"; -import { decodeMessage, encodeMessage } from "@App/pkg/utils/message_value"; +import { decodeRValue, encodeRValue, type REncoded } from "@App/pkg/utils/message_value"; import { type TGMKeyValue } from "@App/app/repo/value"; // 内部函数呼叫定义 @@ -157,8 +157,10 @@ class GM_Base implements IGM_Base { } } if (valueUpdated) { - const valueChanges = decodeMessage(entries); - for (const [key, value, oldValue] of valueChanges) { + 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) { @@ -285,6 +287,7 @@ export default class GMApi extends GM_Base { valueChangePromiseMap.set(id, promise); } const valueStore = a.scriptRes.value; + const keyValuePairs = [] as [string, REncoded][]; for (const [key, value] of Object.entries(values)) { let value_ = value; // 对object的value进行一次转化 @@ -296,10 +299,10 @@ export default class GMApi extends GM_Base { } else { valueStore[key] = value_; } + // 避免undefined 等空值流失,先进行映射处理 + keyValuePairs.push([key, encodeRValue(value)]); } - // 避免undefined 等空值流失,先进行映射处理 - const valuesNew = encodeMessage(values); - a.sendMessage("GM_setValues", [id, valuesNew]); + a.sendMessage("GM_setValues", [id, keyValuePairs]); return id; } diff --git a/src/app/service/content/types.ts b/src/app/service/content/types.ts index e81f25d3f..d4be38d47 100644 --- a/src/app/service/content/types.ts +++ b/src/app/service/content/types.ts @@ -1,4 +1,4 @@ -import type { TEncodedMessage } from "@App/pkg/utils/message_value"; +import type { REncoded } from "@App/pkg/utils/message_value"; import type { ScriptLoadInfo } from "../service_worker/types"; export type ScriptFunc = (named: { [key: string]: any } | undefined, scriptName: string) => any; @@ -15,11 +15,12 @@ export type ValueUpdateSender = { /** * key, value, oldValue */ -export type ValueUpdateDateEntry = [string, any, any]; +export type ValueUpdateDataEntry = [string, any, any]; +export type ValueUpdateDataREntry = [string, REncoded, REncoded]; export type ValueUpdateData = { id?: string; - entries: ValueUpdateDateEntry[]; + entries: ValueUpdateDataEntry[]; uuid: string; storageName: string; // 储存name sender: ValueUpdateSender; @@ -27,7 +28,7 @@ export type ValueUpdateData = { export type ValueUpdateDataEncoded = { id?: string; - entries: TEncodedMessage; + entries: ValueUpdateDataREntry[]; uuid: string; storageName: string; // 储存name sender: ValueUpdateSender; diff --git a/src/app/service/sandbox/runtime.ts b/src/app/service/sandbox/runtime.ts index cbf02fad4..222fe2e66 100644 --- a/src/app/service/sandbox/runtime.ts +++ b/src/app/service/sandbox/runtime.ts @@ -13,12 +13,12 @@ import { CronJob } from "cron"; import { proxyUpdateRunStatus } from "../offscreen/client"; import { BgExecScriptWarp } from "../content/exec_warp"; import type ExecScript from "../content/exec_script"; -import type { ValueUpdateData, ValueUpdateDataEncoded } from "../content/types"; +import type { ValueUpdateDataEncoded } 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"; import { parseUserConfig } from "@App/pkg/utils/yaml"; -import { decodeMessage } from "@App/pkg/utils/message_value"; +import { decodeRValue } from "@App/pkg/utils/message_value"; export class Runtime { cronJob: Map> = new Map(); @@ -324,7 +324,7 @@ export class Runtime { } valueUpdate(data: ValueUpdateDataEncoded) { - const dataNew = { ...data, entries: decodeMessage(data.entries) } as ValueUpdateData; + const dataEntries = data.entries; // 转发给脚本 this.execScripts.forEach((val) => { if (val.scriptRes.uuid === data.uuid || getStorageName(val.scriptRes) === data.storageName) { @@ -334,7 +334,8 @@ export class Runtime { // 更新crontabScripts中的脚本值 for (const script of this.crontabSripts) { if (script.uuid === data.uuid || getStorageName(script) === data.storageName) { - for (const [key, value, _oldValue] of dataNew.entries) { + for (const [key, rTyped1, _rTyped2] of dataEntries) { + const value = decodeRValue(rTyped1); script.value[key] = value; } } diff --git a/src/app/service/service_worker/client.ts b/src/app/service/service_worker/client.ts index 62429a850..b6416fc87 100644 --- a/src/app/service/service_worker/client.ts +++ b/src/app/service/service_worker/client.ts @@ -24,6 +24,7 @@ import type { GMInfoEnv } from "../content/types"; import { type SystemService } from "./system"; import { type ScriptInfo } from "@App/pkg/utils/scriptInstall"; import type { ScriptService, TCheckScriptUpdateOption, TOpenBatchUpdatePageOption } from "./script"; +import { type TKeyValuePair } from "@App/pkg/utils/message_value"; export class ServiceWorkerClient extends Client { constructor(msgSender: MessageSend) { @@ -234,12 +235,12 @@ export class ValueClient extends Client { return this.doThrow("getScriptValue", script); } - setScriptValue(uuid: string, key: string, value: any) { - return this.do("setScriptValue", { uuid, key, value }); + setScriptValue(params: { uuid: string; key: string; value: any }) { + return this.do("setScriptValue", params); } - setScriptValues(uuid: string, values: { [key: string]: any }) { - return this.do("setScriptValues", { uuid, values }); + setScriptValues(params: { uuid: string; keyValuePairs: TKeyValuePair[] }) { + return this.do("setScriptValues", params); } } diff --git a/src/app/service/service_worker/gm_api.ts b/src/app/service/service_worker/gm_api.ts index a0c3fbb18..6fc594f87 100644 --- a/src/app/service/service_worker/gm_api.ts +++ b/src/app/service/service_worker/gm_api.ts @@ -30,8 +30,7 @@ import type { import type { TScriptMenuRegister, TScriptMenuUnregister } from "../queue"; import { BrowserNoSupport, notificationsUpdate } from "./utils"; import i18n from "@App/locales/locales"; -import { decodeMessage, type TEncodedMessage } from "@App/pkg/utils/message_value"; -import { type TGMKeyValue } from "@App/app/repo/value"; +import { type TKeyValuePair } from "@App/pkg/utils/message_value"; import { createObjectURL } from "../offscreen/client"; // GMApi,处理脚本的GM API调用请求 @@ -344,17 +343,16 @@ export default class GMApi { } @PermissionVerify.API({ link: ["GM_deleteValues"] }) - async GM_setValues(request: GMApiRequest<[string, TEncodedMessage]>, sender: IGetSender) { + async GM_setValues(request: GMApiRequest<[string, TKeyValuePair[]]>, sender: IGetSender) { if (!request.params || request.params.length !== 2) { throw new Error("param is failed"); } - const [id, valuesNew] = request.params; - const values = decodeMessage(valuesNew); + const [id, keyValuePairs] = request.params; const valueSender = { runFlag: request.runFlag, tabId: sender.getSender()?.tab?.id || -1, }; - await this.value.setValues(request.script.uuid, id, values, valueSender, false); + await this.value.setValues(request.script.uuid, id, keyValuePairs, valueSender, false); } @PermissionVerify.API() diff --git a/src/app/service/service_worker/value.ts b/src/app/service/service_worker/value.ts index 7de284644..c74cfb31f 100644 --- a/src/app/service/service_worker/value.ts +++ b/src/app/service/service_worker/value.ts @@ -6,13 +6,14 @@ 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, ValueUpdateSender } from "../content/types"; +import type { ValueUpdateDataEncoded, ValueUpdateDataREntry, ValueUpdateSender } from "../content/types"; import type { TScriptValueUpdate } from "../queue"; import { type TDeleteScript } from "../queue"; import { type IMessageQueue } from "@Packages/message/message_queue"; import { CACHE_KEY_SET_VALUE } from "@App/app/cache_key"; import { stackAsyncTask } from "@App/pkg/utils/async_queue"; -import { encodeMessage } from "@App/pkg/utils/message_value"; +import type { TKeyValuePair } from "@App/pkg/utils/message_value"; +import { decodeRValue, R_UNDEFINED, encodeRValue } from "@App/pkg/utils/message_value"; export class ValueService { logger: Logger; @@ -103,12 +104,12 @@ export class ValueService { }); this.pushValueToTab({ id, - entries: encodeMessage([[key, value, oldValue]]), + entries: [[key, encodeRValue(value), encodeRValue(oldValue)]], uuid, storageName, sender, valueUpdated, - } as ValueUpdateDataEncoded); + } satisfies ValueUpdateDataEncoded); // valueUpdate 消息用于 early script 的处理 this.mq.emit("valueUpdate", { script, valueUpdated }); } @@ -153,7 +154,7 @@ export class ValueService { async setValues( uuid: string, id: string, - values: { [key: string]: any }, + keyValuePairs: TKeyValuePair[], sender: ValueUpdateSender, removeNotProvided: boolean ) { @@ -164,15 +165,25 @@ export class ValueService { const storageName = getStorageName(script); let oldValueRecord: { [key: string]: any } = {}; const cacheKey = `${CACHE_KEY_SET_VALUE}${storageName}`; - const entries = [] as [string, any, any][]; + const entries = [] as ValueUpdateDataREntry[]; const _flag = await stackAsyncTask(cacheKey, async () => { let valueModel: Value | undefined = await this.valueDAO.get(storageName); + const now = Date.now(); 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]); + } + } + // 即使是空 dataModel 也进行更新 + // 由于没entries, valueUpdated 是 false, 但 valueDAO 会有一个空的 valueModel 记录 updatetime valueModel = { - uuid: script.uuid, + uuid: uuid, storageName: storageName, - data: values, + data: dataModel, createtime: now, updatetime: now, }; @@ -180,26 +191,30 @@ export class ValueService { let changed = false; let dataModel = (oldValueRecord = valueModel.data); dataModel = { ...dataModel }; // 每次储存使用新参考 - for (const [key, value] of Object.entries(values)) { + 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 (values[key] === undefined) { + if (value === undefined) { delete dataModel[key]; } else { dataModel[key] = value; } - entries.push([key, value, oldValue]); + const rTyped2 = encodeRValue(oldValue); + entries.push([key, rTyped1, rTyped2]); } if (removeNotProvided) { // 处理oldValue有但是没有在data.values中的情况 for (const key of Object.keys(oldValueRecord)) { - if (!(key in values)) { + if (!containedKeys.has(key)) { changed = true; const oldValue = oldValueRecord[key]; delete dataModel[key]; // 这里使用delete是因为保存不需要这个字段了 - values[key] = undefined; // 而这里使用undefined是为了在推送时能够正确处理 - entries.push([key, undefined, oldValue]); + const rTyped2 = encodeRValue(oldValue); + entries.push([key, R_UNDEFINED, rTyped2]); } } } @@ -213,7 +228,7 @@ export class ValueService { const valueUpdated = entries.length > 0; this.pushValueToTab({ id, - entries: encodeMessage(entries), + entries: entries, uuid, storageName, sender, @@ -231,12 +246,12 @@ export class ValueService { return this.setValue(uuid, "", key, value, valueSender); } - setScriptValues({ uuid, values }: { uuid: string; values: { [key: string]: any } }, _sender: IGetSender) { + setScriptValues({ uuid, keyValuePairs }: { uuid: string; keyValuePairs: TKeyValuePair[] }, _sender: IGetSender) { const valueSender = { runFlag: "user", tabId: -2, }; - return this.setValues(uuid, "", values, valueSender, true); + return this.setValues(uuid, "", keyValuePairs, valueSender, true); } init(runtime: RuntimeService, popup: PopupService) { diff --git a/src/pages/components/ScriptStorage/index.tsx b/src/pages/components/ScriptStorage/index.tsx index 2f25446b9..edcfc8b09 100644 --- a/src/pages/components/ScriptStorage/index.tsx +++ b/src/pages/components/ScriptStorage/index.tsx @@ -1,5 +1,7 @@ import type { Script } from "@App/app/repo/scripts"; import { valueClient } from "@App/pages/store/features/script"; +import type { TKeyValuePair } from "@App/pkg/utils/message_value"; +import { encodeRValue } from "@App/pkg/utils/message_value"; import { valueType } from "@App/pkg/utils/utils"; import { Button, Drawer, Form, Input, Message, Modal, Popconfirm, Select, Space, Table } from "@arco-design/web-react"; import type { RefInputType } from "@arco-design/web-react/es/Input/interface"; @@ -33,7 +35,7 @@ const ScriptStorage: React.FC<{ // 保存单个键值 const saveData = (key: string, value: any) => { - valueClient.setScriptValue(script!.uuid, key, value); + valueClient.setScriptValue({ uuid: script!.uuid, key, value }); const newRawData = { ...rawData, [key]: value }; if (value === undefined) { delete newRawData[key]; @@ -43,7 +45,11 @@ const ScriptStorage: React.FC<{ // 保存所有键值 const saveRawData = (newRawValue: { [key: string]: any }) => { - valueClient.setScriptValues(script!.uuid, newRawValue); + const keyValuePairs = [] as TKeyValuePair[]; + for (const [key, value] of Object.entries(newRawValue)) { + keyValuePairs.push([key, encodeRValue(value)]); + } + valueClient.setScriptValues({ uuid: script!.uuid, keyValuePairs }); updateRawData(newRawValue); }; diff --git a/src/pages/components/UserConfigPanel/index.tsx b/src/pages/components/UserConfigPanel/index.tsx index 3a060781a..d22a82bd9 100644 --- a/src/pages/components/UserConfigPanel/index.tsx +++ b/src/pages/components/UserConfigPanel/index.tsx @@ -63,7 +63,11 @@ const UserConfigPanel: React.FC<{ if (saveValues[key][valueKey] === undefined) { continue; } - valueClient.setScriptValue(script.uuid, `${key}.${valueKey}`, saveValues[key][valueKey]); + valueClient.setScriptValue({ + uuid: script.uuid, + key: `${key}.${valueKey}`, + value: saveValues[key][valueKey], + }); } } Message.success(t("save_success")!); // 替换为键值对应的英文文本 diff --git a/src/pkg/utils/message_value.test.ts b/src/pkg/utils/message_value.test.ts index 62d293990..bad8e2a9d 100644 --- a/src/pkg/utils/message_value.test.ts +++ b/src/pkg/utils/message_value.test.ts @@ -1,105 +1,171 @@ -// message_value.test.ts -import { describe, it, expect, vi } from "vitest"; -import { encodeMessage, decodeMessage, type TEncodedMessage } from "./message_value"; - -describe.concurrent("encodeMessage / decodeMessage", () => { - it.concurrent("应能正确编码与解码包含 undefined 和 null 的对象", () => { - const input = { - a: undefined, - b: null, - c: 1, - d: "text", - e: [undefined, null, 2, "ok"], - f: { x: undefined, y: null, z: [1, undefined] }, - }; - const encoded = encodeMessage(input); - const decoded = decodeMessage(encoded); - expect(decoded).toEqual(input); - }); - - it.concurrent("应保持输入对象未被修改", () => { - const input = { a: undefined, b: { c: null } }; - encodeMessage(input); - expect("a" in input).toBe(true); - expect(input.a).toBeUndefined(); - expect("b" in input).toBe(true); - expect("c" in input.b).toBe(true); - expect(input.b.c).toBeNull(); - }); - - it.concurrent("数组中的 undefined 与 null 可被正确往返且索引不丢失", () => { - const input = [1, undefined, null, "x", [undefined, null]]; - const encoded = encodeMessage(input); - const decoded = decodeMessage(encoded) as any[]; - expect(decoded).toEqual(input); - expect(1 in decoded).toBe(true); - expect(4 in decoded).toBe(true); - expect(5 in decoded).toBe(false); - expect(decoded.length).toBe(5); - expect(Array.isArray(decoded)).toBe(true); - }); - - it.concurrent("原始类型应能保持相等", () => { - const nums = 42; - const strs = "hello"; - const bools = false; - expect(decodeMessage(encodeMessage(nums))).toBe(nums); - expect(decodeMessage(encodeMessage(strs))).toBe(strs); - expect(decodeMessage(encodeMessage(bools))).toBe(bools); - }); - - it.concurrent("应能正确处理深层嵌套结构", () => { - const input = { a: { b: { c: { d: undefined, e: null, f: [1, { g: undefined }] } } } }; - const decoded = decodeMessage(encodeMessage(input)); - expect(decoded).toEqual(input); - }); - - it.concurrent("应生成唯一的随机键并正确还原", () => { - const r1 = 0.123456789; - const r2 = 0.987654321; - const spy = vi.spyOn(Math, "random").mockReturnValueOnce(r1).mockReturnValueOnce(r2); - const encoded = encodeMessage({ v: undefined }); - expect(encoded.k.startsWith("##")).toBe(true); - expect(encoded.k.endsWith("##")).toBe(true); - expect(encoded.k.includes(String(r1))).toBe(true); - expect(encoded.k.includes(String(r2))).toBe(true); - const decoded = decodeMessage(encoded as TEncodedMessage<{ v: unknown }>); - expect(decoded).toEqual({ v: undefined }); - spy.mockRestore(); - }); - - it.concurrent("无效输入应抛出异常", () => { - expect(() => decodeMessage({ k: "##x##" } as any)).toThrowError("invalid decodeMessage"); - expect(() => decodeMessage({ m: {} } as any)).toThrowError("invalid decodeMessage"); - expect(() => decodeMessage({ m: {}, k: 123 } as any)).toThrowError("invalid decodeMessage"); - }); - - it.concurrent("不同随机键的占位符不应互相干扰", () => { - const aKey = "##A##"; - const bKey = "##B##"; - const aUndefined = `${aKey}undefined`; - const bNull = `${bKey}null`; - const encodedA: TEncodedMessage = { - k: aKey, - m: { - shouldStayString: bNull, - willBecomeUndef: aUndefined, - }, - }; - const decodedA = decodeMessage(encodedA); - expect(decodedA).toEqual({ - shouldStayString: bNull, - willBecomeUndef: undefined, +import { describe, it, expect } from "vitest"; +import { RType, R_UNDEFINED, R_NULL, decodeRValue, encodeRValue, type REncoded } from "./message_value"; + +describe.concurrent("encodeRValue 编码函数", () => { + it.concurrent("应将 undefined 编码为 R_UNDEFINED", () => { + const encoded = encodeRValue(undefined); + expect(encoded).toEqual(R_UNDEFINED); + expect(encoded[0]).toBe(RType.UNDEFINED); + }); + + it.concurrent("应将 null 编码为 R_NULL", () => { + const encoded = encodeRValue(null); + expect(encoded).toEqual(R_NULL); + expect(encoded[0]).toBe(RType.NULL); + }); + + it.concurrent("应将数字编码为 STANDARD 类型元组", () => { + const value = 123; + const encoded = encodeRValue(value); + expect(encoded[0]).toBe(RType.STANDARD); + expect(encoded[1]).toBe(value); + }); + + it.concurrent("应将字符串编码为 STANDARD 类型元组", () => { + const value = "测试字符串"; + const encoded = encodeRValue(value); + expect(encoded[0]).toBe(RType.STANDARD); + expect(encoded[1]).toBe(value); + }); + + it.concurrent("应将布尔值编码为 STANDARD 类型元组", () => { + const value = true; + const encoded = encodeRValue(value); + expect(encoded[0]).toBe(RType.STANDARD); + expect(encoded[1]).toBe(value); + }); + + it.concurrent("应将对象编码为 STANDARD 类型元组且保持引用", () => { + const obj = { a: 1 }; + const encoded = encodeRValue(obj); + expect(encoded[0]).toBe(RType.STANDARD); + expect(encoded[1]).toBe(obj); + }); + + it.concurrent("应将 symbol 编码为 STANDARD 类型元组", () => { + const sym = Symbol("测试"); + const encoded = encodeRValue(sym as any); + expect(encoded[0]).toBe(RType.STANDARD); + expect(encoded[1]).toBe(sym); + }); + + it.concurrent("应将 bigint 编码为 STANDARD 类型元组", () => { + const big = 10n; + const encoded = encodeRValue(big as any); + expect(encoded[0]).toBe(RType.STANDARD); + expect(encoded[1]).toBe(big); + }); + + it.concurrent("应正确处理联合类型的编码", () => { + const value: string | null = "联合类型测试"; + const encoded = encodeRValue(value); + expect(encoded[0]).toBe(RType.STANDARD); + expect(encoded[1]).toBe(value); + }); +}); + +describe.concurrent("decodeRValue 解码函数", () => { + it.concurrent("应将 R_UNDEFINED 解码为 undefined", () => { + const decoded = decodeRValue(R_UNDEFINED); + expect(decoded).toBeUndefined(); + }); + + it.concurrent("应将 R_NULL 解码为 null", () => { + const decoded = decodeRValue(R_NULL); + expect(decoded).toBeNull(); + }); + + it.concurrent("应将 STANDARD 类型元组解码为原始值(数字)", () => { + const encoded: REncoded = [RType.STANDARD, 42]; + const decoded = decodeRValue(encoded); + expect(decoded).toBe(42); + }); + + it.concurrent("应将 STANDARD 类型元组解码为原始值(字符串)", () => { + const encoded: REncoded = [RType.STANDARD, "解码测试"]; + const decoded = decodeRValue(encoded); + expect(decoded).toBe("解码测试"); + }); + + it.concurrent("应将 STANDARD 类型元组解码为原始值(布尔值)", () => { + const encoded: REncoded = [RType.STANDARD, false]; + const decoded = decodeRValue(encoded); + expect(decoded).toBe(false); + }); + + it.concurrent("应将 STANDARD 类型元组解码为对象并保持引用", () => { + const obj = { x: 99 }; + const encoded: REncoded = [RType.STANDARD, obj]; + const decoded = decodeRValue(encoded); + expect(decoded).toBe(obj); + }); + + it.concurrent("应将 STANDARD 类型元组解码为 symbol", () => { + const sym = Symbol("解码 symbol"); + const encoded: REncoded = [RType.STANDARD, sym]; + const decoded = decodeRValue(encoded); + expect(decoded).toBe(sym); + }); + + it.concurrent("应将 STANDARD 类型元组解码为 bigint", () => { + const big = 123n; + const encoded: REncoded = [RType.STANDARD, big]; + const decoded = decodeRValue(encoded); + expect(decoded).toBe(big); + }); +}); + +describe.concurrent("encodeRValue 与 decodeRValue 组合行为", () => { + it.concurrent("应保证编码解码往返后值保持不变", () => { + const sym = Symbol("往返 symbol"); + const values: any[] = [ + undefined, + null, + 0, + -1, + 3.14, + "往返测试", + "", + true, + false, + { foo: "bar" }, + [1, 2, 3], + sym, + 999n, + ]; + + const roundTrip = values.map((v) => decodeRValue(encodeRValue(v))); + + roundTrip.forEach((decoded, index) => { + const original = values[index]; + if (typeof original === "object" && original !== null) { + expect(decoded).toBe(original); + } else { + expect(decoded).toBe(original); + } }); }); - it.concurrent("普通字符串包含 'undefined' 或 'null' 时不应被误替换", () => { - const input = { - s1: "undefined", - s2: "null", - s3: "##not-the-same##undefined", - }; - const decoded = decodeMessage(encodeMessage(input)); - expect(decoded).toEqual(input); + it.concurrent("应对联合类型值进行正确的往返编码解码", () => { + type Union = string | number | null | undefined; + const values: Union[] = [undefined, null, 1, 0, 123, "abc", ""]; + + const roundTrip = values.map((v) => decodeRValue(encodeRValue(v))); + + expect(roundTrip).toEqual(values); + }); +}); + +describe.concurrent("R_UNDEFINED 与 R_NULL 常量形状", () => { + it.concurrent("R_UNDEFINED 应为只包含 UNDEFINED 类型的单元素元组", () => { + expect(Array.isArray(R_UNDEFINED)).toBe(true); + expect(R_UNDEFINED.length).toBe(1); + expect(R_UNDEFINED[0]).toBe(RType.UNDEFINED); + }); + + it.concurrent("R_NULL 应为只包含 NULL 类型的单元素元组", () => { + expect(Array.isArray(R_NULL)).toBe(true); + expect(R_NULL.length).toBe(1); + expect(R_NULL[0]).toBe(RType.NULL); }); }); diff --git a/src/pkg/utils/message_value.ts b/src/pkg/utils/message_value.ts index 23b672674..56ffc9a11 100644 --- a/src/pkg/utils/message_value.ts +++ b/src/pkg/utils/message_value.ts @@ -1,89 +1,87 @@ /** - * 泛型类型:编码后的消息结构 - * @template T - 任意类型的原始数据 - * @property {T} m - 已编码的消息内容 - * @property {string} k - 用于标识编码批次的随机键 + * RType 枚举 —— 表示编码后的值类型 + * - STANDARD: 标准类型(包含真实值) + * - UNDEFINED: 表示 undefined + * - NULL: 表示 null */ -export type TEncodedMessage = { m: T; k: string }; +export const enum RType { + STANDARD = 0, + UNDEFINED = 1, + NULL = 2, +} /** - * 将对象中的 undefined 和 null 特殊编码,确保可被安全序列化(例如用于 JSON 传输) - * @template T - * @param {T} values - 要编码的对象或数据 - * @returns {TEncodedMessage} - 返回包含编码后数据与唯一键的对象 + * R_UNDEFINED —— 表示编码后的 undefined + * 仅包含一个元素:RType.UNDEFINED */ -export const encodeMessage = (values: T): TEncodedMessage => { - // 生成唯一随机标识符,用于区分 null/undefined 占位 - const sRandomId = `##${Math.random()}${Math.random()}##`; - const sUndefined = `${sRandomId}undefined`; - const sNull = `${sRandomId}null`; +export const R_UNDEFINED = [RType.UNDEFINED] as REncoded; - /** - * 递归编码函数 - * 将对象中的 undefined / null 转为唯一占位符字符串 - * @param {any} input - 任意输入值 - * @returns {any} - 编码后的值 - */ - const enc = (input: any): any => { - // 内联优化:判断类型,提高性能 - if (input === undefined) return sUndefined; - if (input === null) return sNull; - if (typeof input !== "object") return input; +/** + * R_NULL —— 表示编码后的 null + * 仅包含一个元素:RType.NULL + */ +export const R_NULL = [RType.NULL] as REncoded; - // 数组递归处理 - if (Array.isArray(input)) { - return input.map(enc); - } +/** + * REncoded + * 已编码的结果类型,结构为一个定长元组: + * + * - [RType.UNDEFINED] —— 表示 undefined + * - [RType.NULL] —— 表示 null + * - [RType.STANDARD, T] —— 包含真实值 T + * + * @template T 原始值类型 + */ +export type REncoded = [RType.UNDEFINED] | [RType.NULL] | [RType.STANDARD, T]; - // 普通对象递归处理 - const out: Record = {}; - for (const k in input) { - out[k] = enc(input[k]); - } - return out; - }; +/** + * 表示一个 key-value 的键值对,其中 value 为已编码形式 + * @template T 原始值类型 + */ +export type TKeyValuePair = [string, REncoded]; - return { m: enc(values), k: sRandomId }; +/** + * decodeRValue + * 反编码:将已编码的数据恢复为真实值 + * + * @param rTyped 已编码的值 + * @returns 解码后的真实值(undefined | null | T) + * + * @example + * decodeRValue([RType.UNDEFINED]) // undefined + * decodeRValue([RType.NULL]) // null + * decodeRValue([RType.STANDARD, 123]) // 123 + */ +export const decodeRValue = (rTyped: REncoded) => { + switch (rTyped[0]) { + case RType.UNDEFINED: + return undefined; + case RType.NULL: + return null; + default: + return rTyped[1] as T; + } }; /** - * 将 encodeMessage 生成的编码数据还原为原始对象 - * @template T - * @param {TEncodedMessage} values - 编码后的消息对象 - * @returns {T} - 还原后的原始数据 - * @throws {Error} 当输入无效或格式错误时抛出异常 + * encodeRValue + * 编码:将普通值编码为 REncoded 元组,用于稳定传输与序列化。 + * + * @param value 原始值 + * @returns REncoded + * + * @example + * encodeRValue(undefined) // [RType.UNDEFINED] + * encodeRValue(null) // [RType.NULL] + * encodeRValue(123) // [RType.STANDARD, 123] */ -export const decodeMessage = (values: TEncodedMessage): T => { - const { m, k } = values; - if (m === null || m === undefined || !k || typeof k !== "string") throw new Error("invalid decodeMessage"); - - const sRandomId = k; - const sUndefined = `${sRandomId}undefined`; - const sNull = `${sRandomId}null`; - - /** - * 递归解码函数 - * 将占位符字符串还原为 undefined / null - * @param {any} input - 任意输入值 - * @returns {any} - 解码后的值 - */ - const dec = (input: any): any => { - if (input == sUndefined) return undefined; - if (input === sNull) return null; - if (typeof input !== "object") return input; - - // 数组递归处理 - if (Array.isArray(input)) { - return input.map(dec); - } - - // 对象递归处理 - const out: Record = {}; - for (const k in input) { - out[k] = dec(input[k]); - } - return out; - }; - - return dec(m); +export const encodeRValue = (value: T): REncoded => { + switch (value) { + case undefined: + return R_UNDEFINED as [RType.UNDEFINED]; + case null: + return R_NULL as [RType.NULL]; + default: + return [RType.STANDARD, value] as [RType.STANDARD, T]; + } }; From 51246cb876f54d34151bb08fe3199147ed2c02ab Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Thu, 13 Nov 2025 21:01:18 +0900 Subject: [PATCH 2/9] encode/decodeMessage -> encode/decodeRValue --- src/app/service/content/gm_api.test.ts | 78 +++-- src/app/service/content/gm_api.ts | 15 +- src/app/service/content/types.ts | 9 +- src/app/service/sandbox/runtime.ts | 9 +- src/app/service/service_worker/client.ts | 9 +- src/app/service/service_worker/gm_api.ts | 10 +- src/app/service/service_worker/value.ts | 51 ++-- src/pages/components/ScriptStorage/index.tsx | 10 +- .../components/UserConfigPanel/index.tsx | 6 +- src/pkg/utils/message_value.test.ts | 266 +++++++++++------- src/pkg/utils/message_value.ts | 148 +++++----- 11 files changed, 349 insertions(+), 262 deletions(-) diff --git a/src/app/service/content/gm_api.test.ts b/src/app/service/content/gm_api.test.ts index 05e8b8f24..28b4ddb3e 100644 --- a/src/app/service/content/gm_api.test.ts +++ b/src/app/service/content/gm_api.test.ts @@ -4,7 +4,7 @@ import type { ScriptLoadInfo } from "../service_worker/types"; import type { GMInfoEnv, ScriptFunc } from "./types"; import { compileScript, compileScriptCode } from "./utils"; import type { Message } from "@Packages/message/types"; -import { encodeMessage } from "@App/pkg/utils/message_value"; +import { encodeRValue } from "@App/pkg/utils/message_value"; import { v4 as uuidv4 } from "uuid"; const nilFn: ScriptFunc = () => {}; @@ -484,6 +484,12 @@ describe.concurrent("GM_value", () => { expect(mockSendMessage).toHaveBeenCalled(); expect(mockSendMessage).toHaveBeenCalledTimes(2); + const keyValuePairs1 = [ + ["a", encodeRValue(123)], + ["b", encodeRValue(456)], + ["c", encodeRValue("789")], + ]; + // 第一次调用:设置值为 123 expect(mockSendMessage).toHaveBeenNthCalledWith( 1, @@ -495,14 +501,7 @@ describe.concurrent("GM_value", () => { // event id expect.stringMatching(/^.+::\d$/), // the object payload - expect.objectContaining({ - k: expect.stringMatching(/^##[\d.]+##$/), - m: expect.objectContaining({ - a: 123, - b: 456, - c: "789", - }), - }), + keyValuePairs1, ], runFlag: expect.any(String), uuid: undefined, @@ -510,6 +509,11 @@ describe.concurrent("GM_value", () => { }) ); + const keyValuePairs2 = [ + ["a", encodeRValue(undefined)], + ["c", encodeRValue(undefined)], + ]; + // 第二次调用:删除值(设置为 undefined) expect(mockSendMessage).toHaveBeenNthCalledWith( 2, @@ -521,13 +525,7 @@ describe.concurrent("GM_value", () => { // event id expect.stringMatching(/^.+::\d$/), // the object payload - expect.objectContaining({ - k: expect.stringMatching(/^##[\d.]+##$/), - m: expect.objectContaining({ - a: expect.stringMatching(/^##[\d.]+##undefined$/), - c: expect.stringMatching(/^##[\d.]+##undefined$/), - }), - }), + keyValuePairs2, ], runFlag: expect.any(String), uuid: undefined, @@ -561,6 +559,11 @@ describe.concurrent("GM_value", () => { expect(mockSendMessage).toHaveBeenCalled(); expect(mockSendMessage).toHaveBeenCalledTimes(2); + const keyValuePairs1 = [ + ["a", encodeRValue(123)], + ["b", encodeRValue(456)], + ["c", encodeRValue("789")], + ]; // 第一次调用:设置值为 123 expect(mockSendMessage).toHaveBeenNthCalledWith( 1, @@ -572,14 +575,7 @@ describe.concurrent("GM_value", () => { // event id expect.stringMatching(/^.+::\d$/), // the object payload - expect.objectContaining({ - k: expect.stringMatching(/^##[\d.]+##$/), - m: expect.objectContaining({ - a: 123, - b: 456, - c: "789", - }), - }), + keyValuePairs1, ], runFlag: expect.any(String), uuid: undefined, @@ -632,6 +628,12 @@ describe.concurrent("GM_value", () => { expect(mockSendMessage).toHaveBeenCalled(); expect(mockSendMessage).toHaveBeenCalledTimes(2); + const keyValuePairs1 = [ + ["a", encodeRValue(123)], + ["b", encodeRValue(456)], + ["c", encodeRValue("789")], + ]; + // 第一次调用:设置值为 123 expect(mockSendMessage).toHaveBeenNthCalledWith( 1, @@ -643,14 +645,7 @@ describe.concurrent("GM_value", () => { // event id expect.stringMatching(/^.+::\d$/), // the object payload - expect.objectContaining({ - k: expect.stringMatching(/^##[\d.]+##$/), - m: expect.objectContaining({ - a: 123, - b: 456, - c: "789", - }), - }), + keyValuePairs1, ], runFlag: expect.any(String), uuid: undefined, @@ -658,6 +653,11 @@ describe.concurrent("GM_value", () => { }) ); + const keyValuePairs2 = [ + ["a", encodeRValue(undefined)], + ["c", encodeRValue(undefined)], + ]; + // 第二次调用:删除值(设置为 undefined) expect(mockSendMessage).toHaveBeenNthCalledWith( 2, @@ -669,13 +669,7 @@ describe.concurrent("GM_value", () => { // event id expect.stringMatching(/^.+::\d$/), // the string payload - expect.objectContaining({ - k: expect.stringMatching(/^##[\d.]+##$/), - m: expect.objectContaining({ - a: expect.stringMatching(/^##[\d.]+##undefined$/), - c: expect.stringMatching(/^##[\d.]+##undefined$/), - }), - }), + keyValuePairs2, ], runFlag: expect.any(String), uuid: undefined, @@ -710,7 +704,7 @@ describe.concurrent("GM_value", () => { // 模拟值变化 exec.valueUpdate({ id: "id-1", - entries: encodeMessage([["param1", 123, undefined]]), + entries: [["param1", encodeRValue(123), encodeRValue(undefined)]], uuid: script.uuid, storageName: script.uuid, sender: { runFlag: exec.sandboxContext!.runFlag, tabId: -2 }, @@ -745,7 +739,7 @@ describe.concurrent("GM_value", () => { // 模拟值变化 exec.valueUpdate({ id: "id-2", - entries: encodeMessage([["param2", 456, undefined]]), + entries: [["param2", encodeRValue(456), encodeRValue(undefined)]], uuid: script.uuid, storageName: "testStorage", sender: { runFlag: "user", tabId: -2 }, @@ -780,7 +774,7 @@ describe.concurrent("GM_value", () => { // 触发valueUpdate exec.valueUpdate({ id: id, - entries: encodeMessage([["a", 123, undefined]]), + entries: [["a", encodeRValue(123), encodeRValue(undefined)]], uuid: script.uuid, storageName: script.uuid, sender: { runFlag: exec.sandboxContext!.runFlag, tabId: -2 }, diff --git a/src/app/service/content/gm_api.ts b/src/app/service/content/gm_api.ts index 3eaf219f9..a630d96d8 100644 --- a/src/app/service/content/gm_api.ts +++ b/src/app/service/content/gm_api.ts @@ -19,7 +19,7 @@ import type { MessageRequest } from "../service_worker/types"; import { connect, sendMessage } from "@Packages/message/client"; import { getStorageName } from "@App/pkg/utils/utils"; import { ListenerManager } from "./listener_manager"; -import { decodeMessage, encodeMessage } from "@App/pkg/utils/message_value"; +import { decodeRValue, encodeRValue, type REncoded } from "@App/pkg/utils/message_value"; import { type TGMKeyValue } from "@App/app/repo/value"; // 内部函数呼叫定义 @@ -157,8 +157,10 @@ class GM_Base implements IGM_Base { } } if (valueUpdated) { - const valueChanges = decodeMessage(entries); - for (const [key, value, oldValue] of valueChanges) { + 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) { @@ -285,6 +287,7 @@ export default class GMApi extends GM_Base { valueChangePromiseMap.set(id, promise); } const valueStore = a.scriptRes.value; + const keyValuePairs = [] as [string, REncoded][]; for (const [key, value] of Object.entries(values)) { let value_ = value; // 对object的value进行一次转化 @@ -296,10 +299,10 @@ export default class GMApi extends GM_Base { } else { valueStore[key] = value_; } + // 避免undefined 等空值流失,先进行映射处理 + keyValuePairs.push([key, encodeRValue(value)]); } - // 避免undefined 等空值流失,先进行映射处理 - const valuesNew = encodeMessage(values); - a.sendMessage("GM_setValues", [id, valuesNew]); + a.sendMessage("GM_setValues", [id, keyValuePairs]); return id; } diff --git a/src/app/service/content/types.ts b/src/app/service/content/types.ts index e81f25d3f..d4be38d47 100644 --- a/src/app/service/content/types.ts +++ b/src/app/service/content/types.ts @@ -1,4 +1,4 @@ -import type { TEncodedMessage } from "@App/pkg/utils/message_value"; +import type { REncoded } from "@App/pkg/utils/message_value"; import type { ScriptLoadInfo } from "../service_worker/types"; export type ScriptFunc = (named: { [key: string]: any } | undefined, scriptName: string) => any; @@ -15,11 +15,12 @@ export type ValueUpdateSender = { /** * key, value, oldValue */ -export type ValueUpdateDateEntry = [string, any, any]; +export type ValueUpdateDataEntry = [string, any, any]; +export type ValueUpdateDataREntry = [string, REncoded, REncoded]; export type ValueUpdateData = { id?: string; - entries: ValueUpdateDateEntry[]; + entries: ValueUpdateDataEntry[]; uuid: string; storageName: string; // 储存name sender: ValueUpdateSender; @@ -27,7 +28,7 @@ export type ValueUpdateData = { export type ValueUpdateDataEncoded = { id?: string; - entries: TEncodedMessage; + entries: ValueUpdateDataREntry[]; uuid: string; storageName: string; // 储存name sender: ValueUpdateSender; diff --git a/src/app/service/sandbox/runtime.ts b/src/app/service/sandbox/runtime.ts index cbf02fad4..222fe2e66 100644 --- a/src/app/service/sandbox/runtime.ts +++ b/src/app/service/sandbox/runtime.ts @@ -13,12 +13,12 @@ import { CronJob } from "cron"; import { proxyUpdateRunStatus } from "../offscreen/client"; import { BgExecScriptWarp } from "../content/exec_warp"; import type ExecScript from "../content/exec_script"; -import type { ValueUpdateData, ValueUpdateDataEncoded } from "../content/types"; +import type { ValueUpdateDataEncoded } 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"; import { parseUserConfig } from "@App/pkg/utils/yaml"; -import { decodeMessage } from "@App/pkg/utils/message_value"; +import { decodeRValue } from "@App/pkg/utils/message_value"; export class Runtime { cronJob: Map> = new Map(); @@ -324,7 +324,7 @@ export class Runtime { } valueUpdate(data: ValueUpdateDataEncoded) { - const dataNew = { ...data, entries: decodeMessage(data.entries) } as ValueUpdateData; + const dataEntries = data.entries; // 转发给脚本 this.execScripts.forEach((val) => { if (val.scriptRes.uuid === data.uuid || getStorageName(val.scriptRes) === data.storageName) { @@ -334,7 +334,8 @@ export class Runtime { // 更新crontabScripts中的脚本值 for (const script of this.crontabSripts) { if (script.uuid === data.uuid || getStorageName(script) === data.storageName) { - for (const [key, value, _oldValue] of dataNew.entries) { + for (const [key, rTyped1, _rTyped2] of dataEntries) { + const value = decodeRValue(rTyped1); script.value[key] = value; } } diff --git a/src/app/service/service_worker/client.ts b/src/app/service/service_worker/client.ts index 62429a850..b6416fc87 100644 --- a/src/app/service/service_worker/client.ts +++ b/src/app/service/service_worker/client.ts @@ -24,6 +24,7 @@ import type { GMInfoEnv } from "../content/types"; import { type SystemService } from "./system"; import { type ScriptInfo } from "@App/pkg/utils/scriptInstall"; import type { ScriptService, TCheckScriptUpdateOption, TOpenBatchUpdatePageOption } from "./script"; +import { type TKeyValuePair } from "@App/pkg/utils/message_value"; export class ServiceWorkerClient extends Client { constructor(msgSender: MessageSend) { @@ -234,12 +235,12 @@ export class ValueClient extends Client { return this.doThrow("getScriptValue", script); } - setScriptValue(uuid: string, key: string, value: any) { - return this.do("setScriptValue", { uuid, key, value }); + setScriptValue(params: { uuid: string; key: string; value: any }) { + return this.do("setScriptValue", params); } - setScriptValues(uuid: string, values: { [key: string]: any }) { - return this.do("setScriptValues", { uuid, values }); + setScriptValues(params: { uuid: string; keyValuePairs: TKeyValuePair[] }) { + return this.do("setScriptValues", params); } } diff --git a/src/app/service/service_worker/gm_api.ts b/src/app/service/service_worker/gm_api.ts index a0c3fbb18..6fc594f87 100644 --- a/src/app/service/service_worker/gm_api.ts +++ b/src/app/service/service_worker/gm_api.ts @@ -30,8 +30,7 @@ import type { import type { TScriptMenuRegister, TScriptMenuUnregister } from "../queue"; import { BrowserNoSupport, notificationsUpdate } from "./utils"; import i18n from "@App/locales/locales"; -import { decodeMessage, type TEncodedMessage } from "@App/pkg/utils/message_value"; -import { type TGMKeyValue } from "@App/app/repo/value"; +import { type TKeyValuePair } from "@App/pkg/utils/message_value"; import { createObjectURL } from "../offscreen/client"; // GMApi,处理脚本的GM API调用请求 @@ -344,17 +343,16 @@ export default class GMApi { } @PermissionVerify.API({ link: ["GM_deleteValues"] }) - async GM_setValues(request: GMApiRequest<[string, TEncodedMessage]>, sender: IGetSender) { + async GM_setValues(request: GMApiRequest<[string, TKeyValuePair[]]>, sender: IGetSender) { if (!request.params || request.params.length !== 2) { throw new Error("param is failed"); } - const [id, valuesNew] = request.params; - const values = decodeMessage(valuesNew); + const [id, keyValuePairs] = request.params; const valueSender = { runFlag: request.runFlag, tabId: sender.getSender()?.tab?.id || -1, }; - await this.value.setValues(request.script.uuid, id, values, valueSender, false); + await this.value.setValues(request.script.uuid, id, keyValuePairs, valueSender, false); } @PermissionVerify.API() diff --git a/src/app/service/service_worker/value.ts b/src/app/service/service_worker/value.ts index 7de284644..c74cfb31f 100644 --- a/src/app/service/service_worker/value.ts +++ b/src/app/service/service_worker/value.ts @@ -6,13 +6,14 @@ 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, ValueUpdateSender } from "../content/types"; +import type { ValueUpdateDataEncoded, ValueUpdateDataREntry, ValueUpdateSender } from "../content/types"; import type { TScriptValueUpdate } from "../queue"; import { type TDeleteScript } from "../queue"; import { type IMessageQueue } from "@Packages/message/message_queue"; import { CACHE_KEY_SET_VALUE } from "@App/app/cache_key"; import { stackAsyncTask } from "@App/pkg/utils/async_queue"; -import { encodeMessage } from "@App/pkg/utils/message_value"; +import type { TKeyValuePair } from "@App/pkg/utils/message_value"; +import { decodeRValue, R_UNDEFINED, encodeRValue } from "@App/pkg/utils/message_value"; export class ValueService { logger: Logger; @@ -103,12 +104,12 @@ export class ValueService { }); this.pushValueToTab({ id, - entries: encodeMessage([[key, value, oldValue]]), + entries: [[key, encodeRValue(value), encodeRValue(oldValue)]], uuid, storageName, sender, valueUpdated, - } as ValueUpdateDataEncoded); + } satisfies ValueUpdateDataEncoded); // valueUpdate 消息用于 early script 的处理 this.mq.emit("valueUpdate", { script, valueUpdated }); } @@ -153,7 +154,7 @@ export class ValueService { async setValues( uuid: string, id: string, - values: { [key: string]: any }, + keyValuePairs: TKeyValuePair[], sender: ValueUpdateSender, removeNotProvided: boolean ) { @@ -164,15 +165,25 @@ export class ValueService { const storageName = getStorageName(script); let oldValueRecord: { [key: string]: any } = {}; const cacheKey = `${CACHE_KEY_SET_VALUE}${storageName}`; - const entries = [] as [string, any, any][]; + const entries = [] as ValueUpdateDataREntry[]; const _flag = await stackAsyncTask(cacheKey, async () => { let valueModel: Value | undefined = await this.valueDAO.get(storageName); + const now = Date.now(); 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]); + } + } + // 即使是空 dataModel 也进行更新 + // 由于没entries, valueUpdated 是 false, 但 valueDAO 会有一个空的 valueModel 记录 updatetime valueModel = { - uuid: script.uuid, + uuid: uuid, storageName: storageName, - data: values, + data: dataModel, createtime: now, updatetime: now, }; @@ -180,26 +191,30 @@ export class ValueService { let changed = false; let dataModel = (oldValueRecord = valueModel.data); dataModel = { ...dataModel }; // 每次储存使用新参考 - for (const [key, value] of Object.entries(values)) { + 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 (values[key] === undefined) { + if (value === undefined) { delete dataModel[key]; } else { dataModel[key] = value; } - entries.push([key, value, oldValue]); + const rTyped2 = encodeRValue(oldValue); + entries.push([key, rTyped1, rTyped2]); } if (removeNotProvided) { // 处理oldValue有但是没有在data.values中的情况 for (const key of Object.keys(oldValueRecord)) { - if (!(key in values)) { + if (!containedKeys.has(key)) { changed = true; const oldValue = oldValueRecord[key]; delete dataModel[key]; // 这里使用delete是因为保存不需要这个字段了 - values[key] = undefined; // 而这里使用undefined是为了在推送时能够正确处理 - entries.push([key, undefined, oldValue]); + const rTyped2 = encodeRValue(oldValue); + entries.push([key, R_UNDEFINED, rTyped2]); } } } @@ -213,7 +228,7 @@ export class ValueService { const valueUpdated = entries.length > 0; this.pushValueToTab({ id, - entries: encodeMessage(entries), + entries: entries, uuid, storageName, sender, @@ -231,12 +246,12 @@ export class ValueService { return this.setValue(uuid, "", key, value, valueSender); } - setScriptValues({ uuid, values }: { uuid: string; values: { [key: string]: any } }, _sender: IGetSender) { + setScriptValues({ uuid, keyValuePairs }: { uuid: string; keyValuePairs: TKeyValuePair[] }, _sender: IGetSender) { const valueSender = { runFlag: "user", tabId: -2, }; - return this.setValues(uuid, "", values, valueSender, true); + return this.setValues(uuid, "", keyValuePairs, valueSender, true); } init(runtime: RuntimeService, popup: PopupService) { diff --git a/src/pages/components/ScriptStorage/index.tsx b/src/pages/components/ScriptStorage/index.tsx index 2f25446b9..edcfc8b09 100644 --- a/src/pages/components/ScriptStorage/index.tsx +++ b/src/pages/components/ScriptStorage/index.tsx @@ -1,5 +1,7 @@ import type { Script } from "@App/app/repo/scripts"; import { valueClient } from "@App/pages/store/features/script"; +import type { TKeyValuePair } from "@App/pkg/utils/message_value"; +import { encodeRValue } from "@App/pkg/utils/message_value"; import { valueType } from "@App/pkg/utils/utils"; import { Button, Drawer, Form, Input, Message, Modal, Popconfirm, Select, Space, Table } from "@arco-design/web-react"; import type { RefInputType } from "@arco-design/web-react/es/Input/interface"; @@ -33,7 +35,7 @@ const ScriptStorage: React.FC<{ // 保存单个键值 const saveData = (key: string, value: any) => { - valueClient.setScriptValue(script!.uuid, key, value); + valueClient.setScriptValue({ uuid: script!.uuid, key, value }); const newRawData = { ...rawData, [key]: value }; if (value === undefined) { delete newRawData[key]; @@ -43,7 +45,11 @@ const ScriptStorage: React.FC<{ // 保存所有键值 const saveRawData = (newRawValue: { [key: string]: any }) => { - valueClient.setScriptValues(script!.uuid, newRawValue); + const keyValuePairs = [] as TKeyValuePair[]; + for (const [key, value] of Object.entries(newRawValue)) { + keyValuePairs.push([key, encodeRValue(value)]); + } + valueClient.setScriptValues({ uuid: script!.uuid, keyValuePairs }); updateRawData(newRawValue); }; diff --git a/src/pages/components/UserConfigPanel/index.tsx b/src/pages/components/UserConfigPanel/index.tsx index 3a060781a..d22a82bd9 100644 --- a/src/pages/components/UserConfigPanel/index.tsx +++ b/src/pages/components/UserConfigPanel/index.tsx @@ -63,7 +63,11 @@ const UserConfigPanel: React.FC<{ if (saveValues[key][valueKey] === undefined) { continue; } - valueClient.setScriptValue(script.uuid, `${key}.${valueKey}`, saveValues[key][valueKey]); + valueClient.setScriptValue({ + uuid: script.uuid, + key: `${key}.${valueKey}`, + value: saveValues[key][valueKey], + }); } } Message.success(t("save_success")!); // 替换为键值对应的英文文本 diff --git a/src/pkg/utils/message_value.test.ts b/src/pkg/utils/message_value.test.ts index 62d293990..bad8e2a9d 100644 --- a/src/pkg/utils/message_value.test.ts +++ b/src/pkg/utils/message_value.test.ts @@ -1,105 +1,171 @@ -// message_value.test.ts -import { describe, it, expect, vi } from "vitest"; -import { encodeMessage, decodeMessage, type TEncodedMessage } from "./message_value"; - -describe.concurrent("encodeMessage / decodeMessage", () => { - it.concurrent("应能正确编码与解码包含 undefined 和 null 的对象", () => { - const input = { - a: undefined, - b: null, - c: 1, - d: "text", - e: [undefined, null, 2, "ok"], - f: { x: undefined, y: null, z: [1, undefined] }, - }; - const encoded = encodeMessage(input); - const decoded = decodeMessage(encoded); - expect(decoded).toEqual(input); - }); - - it.concurrent("应保持输入对象未被修改", () => { - const input = { a: undefined, b: { c: null } }; - encodeMessage(input); - expect("a" in input).toBe(true); - expect(input.a).toBeUndefined(); - expect("b" in input).toBe(true); - expect("c" in input.b).toBe(true); - expect(input.b.c).toBeNull(); - }); - - it.concurrent("数组中的 undefined 与 null 可被正确往返且索引不丢失", () => { - const input = [1, undefined, null, "x", [undefined, null]]; - const encoded = encodeMessage(input); - const decoded = decodeMessage(encoded) as any[]; - expect(decoded).toEqual(input); - expect(1 in decoded).toBe(true); - expect(4 in decoded).toBe(true); - expect(5 in decoded).toBe(false); - expect(decoded.length).toBe(5); - expect(Array.isArray(decoded)).toBe(true); - }); - - it.concurrent("原始类型应能保持相等", () => { - const nums = 42; - const strs = "hello"; - const bools = false; - expect(decodeMessage(encodeMessage(nums))).toBe(nums); - expect(decodeMessage(encodeMessage(strs))).toBe(strs); - expect(decodeMessage(encodeMessage(bools))).toBe(bools); - }); - - it.concurrent("应能正确处理深层嵌套结构", () => { - const input = { a: { b: { c: { d: undefined, e: null, f: [1, { g: undefined }] } } } }; - const decoded = decodeMessage(encodeMessage(input)); - expect(decoded).toEqual(input); - }); - - it.concurrent("应生成唯一的随机键并正确还原", () => { - const r1 = 0.123456789; - const r2 = 0.987654321; - const spy = vi.spyOn(Math, "random").mockReturnValueOnce(r1).mockReturnValueOnce(r2); - const encoded = encodeMessage({ v: undefined }); - expect(encoded.k.startsWith("##")).toBe(true); - expect(encoded.k.endsWith("##")).toBe(true); - expect(encoded.k.includes(String(r1))).toBe(true); - expect(encoded.k.includes(String(r2))).toBe(true); - const decoded = decodeMessage(encoded as TEncodedMessage<{ v: unknown }>); - expect(decoded).toEqual({ v: undefined }); - spy.mockRestore(); - }); - - it.concurrent("无效输入应抛出异常", () => { - expect(() => decodeMessage({ k: "##x##" } as any)).toThrowError("invalid decodeMessage"); - expect(() => decodeMessage({ m: {} } as any)).toThrowError("invalid decodeMessage"); - expect(() => decodeMessage({ m: {}, k: 123 } as any)).toThrowError("invalid decodeMessage"); - }); - - it.concurrent("不同随机键的占位符不应互相干扰", () => { - const aKey = "##A##"; - const bKey = "##B##"; - const aUndefined = `${aKey}undefined`; - const bNull = `${bKey}null`; - const encodedA: TEncodedMessage = { - k: aKey, - m: { - shouldStayString: bNull, - willBecomeUndef: aUndefined, - }, - }; - const decodedA = decodeMessage(encodedA); - expect(decodedA).toEqual({ - shouldStayString: bNull, - willBecomeUndef: undefined, +import { describe, it, expect } from "vitest"; +import { RType, R_UNDEFINED, R_NULL, decodeRValue, encodeRValue, type REncoded } from "./message_value"; + +describe.concurrent("encodeRValue 编码函数", () => { + it.concurrent("应将 undefined 编码为 R_UNDEFINED", () => { + const encoded = encodeRValue(undefined); + expect(encoded).toEqual(R_UNDEFINED); + expect(encoded[0]).toBe(RType.UNDEFINED); + }); + + it.concurrent("应将 null 编码为 R_NULL", () => { + const encoded = encodeRValue(null); + expect(encoded).toEqual(R_NULL); + expect(encoded[0]).toBe(RType.NULL); + }); + + it.concurrent("应将数字编码为 STANDARD 类型元组", () => { + const value = 123; + const encoded = encodeRValue(value); + expect(encoded[0]).toBe(RType.STANDARD); + expect(encoded[1]).toBe(value); + }); + + it.concurrent("应将字符串编码为 STANDARD 类型元组", () => { + const value = "测试字符串"; + const encoded = encodeRValue(value); + expect(encoded[0]).toBe(RType.STANDARD); + expect(encoded[1]).toBe(value); + }); + + it.concurrent("应将布尔值编码为 STANDARD 类型元组", () => { + const value = true; + const encoded = encodeRValue(value); + expect(encoded[0]).toBe(RType.STANDARD); + expect(encoded[1]).toBe(value); + }); + + it.concurrent("应将对象编码为 STANDARD 类型元组且保持引用", () => { + const obj = { a: 1 }; + const encoded = encodeRValue(obj); + expect(encoded[0]).toBe(RType.STANDARD); + expect(encoded[1]).toBe(obj); + }); + + it.concurrent("应将 symbol 编码为 STANDARD 类型元组", () => { + const sym = Symbol("测试"); + const encoded = encodeRValue(sym as any); + expect(encoded[0]).toBe(RType.STANDARD); + expect(encoded[1]).toBe(sym); + }); + + it.concurrent("应将 bigint 编码为 STANDARD 类型元组", () => { + const big = 10n; + const encoded = encodeRValue(big as any); + expect(encoded[0]).toBe(RType.STANDARD); + expect(encoded[1]).toBe(big); + }); + + it.concurrent("应正确处理联合类型的编码", () => { + const value: string | null = "联合类型测试"; + const encoded = encodeRValue(value); + expect(encoded[0]).toBe(RType.STANDARD); + expect(encoded[1]).toBe(value); + }); +}); + +describe.concurrent("decodeRValue 解码函数", () => { + it.concurrent("应将 R_UNDEFINED 解码为 undefined", () => { + const decoded = decodeRValue(R_UNDEFINED); + expect(decoded).toBeUndefined(); + }); + + it.concurrent("应将 R_NULL 解码为 null", () => { + const decoded = decodeRValue(R_NULL); + expect(decoded).toBeNull(); + }); + + it.concurrent("应将 STANDARD 类型元组解码为原始值(数字)", () => { + const encoded: REncoded = [RType.STANDARD, 42]; + const decoded = decodeRValue(encoded); + expect(decoded).toBe(42); + }); + + it.concurrent("应将 STANDARD 类型元组解码为原始值(字符串)", () => { + const encoded: REncoded = [RType.STANDARD, "解码测试"]; + const decoded = decodeRValue(encoded); + expect(decoded).toBe("解码测试"); + }); + + it.concurrent("应将 STANDARD 类型元组解码为原始值(布尔值)", () => { + const encoded: REncoded = [RType.STANDARD, false]; + const decoded = decodeRValue(encoded); + expect(decoded).toBe(false); + }); + + it.concurrent("应将 STANDARD 类型元组解码为对象并保持引用", () => { + const obj = { x: 99 }; + const encoded: REncoded = [RType.STANDARD, obj]; + const decoded = decodeRValue(encoded); + expect(decoded).toBe(obj); + }); + + it.concurrent("应将 STANDARD 类型元组解码为 symbol", () => { + const sym = Symbol("解码 symbol"); + const encoded: REncoded = [RType.STANDARD, sym]; + const decoded = decodeRValue(encoded); + expect(decoded).toBe(sym); + }); + + it.concurrent("应将 STANDARD 类型元组解码为 bigint", () => { + const big = 123n; + const encoded: REncoded = [RType.STANDARD, big]; + const decoded = decodeRValue(encoded); + expect(decoded).toBe(big); + }); +}); + +describe.concurrent("encodeRValue 与 decodeRValue 组合行为", () => { + it.concurrent("应保证编码解码往返后值保持不变", () => { + const sym = Symbol("往返 symbol"); + const values: any[] = [ + undefined, + null, + 0, + -1, + 3.14, + "往返测试", + "", + true, + false, + { foo: "bar" }, + [1, 2, 3], + sym, + 999n, + ]; + + const roundTrip = values.map((v) => decodeRValue(encodeRValue(v))); + + roundTrip.forEach((decoded, index) => { + const original = values[index]; + if (typeof original === "object" && original !== null) { + expect(decoded).toBe(original); + } else { + expect(decoded).toBe(original); + } }); }); - it.concurrent("普通字符串包含 'undefined' 或 'null' 时不应被误替换", () => { - const input = { - s1: "undefined", - s2: "null", - s3: "##not-the-same##undefined", - }; - const decoded = decodeMessage(encodeMessage(input)); - expect(decoded).toEqual(input); + it.concurrent("应对联合类型值进行正确的往返编码解码", () => { + type Union = string | number | null | undefined; + const values: Union[] = [undefined, null, 1, 0, 123, "abc", ""]; + + const roundTrip = values.map((v) => decodeRValue(encodeRValue(v))); + + expect(roundTrip).toEqual(values); + }); +}); + +describe.concurrent("R_UNDEFINED 与 R_NULL 常量形状", () => { + it.concurrent("R_UNDEFINED 应为只包含 UNDEFINED 类型的单元素元组", () => { + expect(Array.isArray(R_UNDEFINED)).toBe(true); + expect(R_UNDEFINED.length).toBe(1); + expect(R_UNDEFINED[0]).toBe(RType.UNDEFINED); + }); + + it.concurrent("R_NULL 应为只包含 NULL 类型的单元素元组", () => { + expect(Array.isArray(R_NULL)).toBe(true); + expect(R_NULL.length).toBe(1); + expect(R_NULL[0]).toBe(RType.NULL); }); }); diff --git a/src/pkg/utils/message_value.ts b/src/pkg/utils/message_value.ts index 23b672674..56ffc9a11 100644 --- a/src/pkg/utils/message_value.ts +++ b/src/pkg/utils/message_value.ts @@ -1,89 +1,87 @@ /** - * 泛型类型:编码后的消息结构 - * @template T - 任意类型的原始数据 - * @property {T} m - 已编码的消息内容 - * @property {string} k - 用于标识编码批次的随机键 + * RType 枚举 —— 表示编码后的值类型 + * - STANDARD: 标准类型(包含真实值) + * - UNDEFINED: 表示 undefined + * - NULL: 表示 null */ -export type TEncodedMessage = { m: T; k: string }; +export const enum RType { + STANDARD = 0, + UNDEFINED = 1, + NULL = 2, +} /** - * 将对象中的 undefined 和 null 特殊编码,确保可被安全序列化(例如用于 JSON 传输) - * @template T - * @param {T} values - 要编码的对象或数据 - * @returns {TEncodedMessage} - 返回包含编码后数据与唯一键的对象 + * R_UNDEFINED —— 表示编码后的 undefined + * 仅包含一个元素:RType.UNDEFINED */ -export const encodeMessage = (values: T): TEncodedMessage => { - // 生成唯一随机标识符,用于区分 null/undefined 占位 - const sRandomId = `##${Math.random()}${Math.random()}##`; - const sUndefined = `${sRandomId}undefined`; - const sNull = `${sRandomId}null`; +export const R_UNDEFINED = [RType.UNDEFINED] as REncoded; - /** - * 递归编码函数 - * 将对象中的 undefined / null 转为唯一占位符字符串 - * @param {any} input - 任意输入值 - * @returns {any} - 编码后的值 - */ - const enc = (input: any): any => { - // 内联优化:判断类型,提高性能 - if (input === undefined) return sUndefined; - if (input === null) return sNull; - if (typeof input !== "object") return input; +/** + * R_NULL —— 表示编码后的 null + * 仅包含一个元素:RType.NULL + */ +export const R_NULL = [RType.NULL] as REncoded; - // 数组递归处理 - if (Array.isArray(input)) { - return input.map(enc); - } +/** + * REncoded + * 已编码的结果类型,结构为一个定长元组: + * + * - [RType.UNDEFINED] —— 表示 undefined + * - [RType.NULL] —— 表示 null + * - [RType.STANDARD, T] —— 包含真实值 T + * + * @template T 原始值类型 + */ +export type REncoded = [RType.UNDEFINED] | [RType.NULL] | [RType.STANDARD, T]; - // 普通对象递归处理 - const out: Record = {}; - for (const k in input) { - out[k] = enc(input[k]); - } - return out; - }; +/** + * 表示一个 key-value 的键值对,其中 value 为已编码形式 + * @template T 原始值类型 + */ +export type TKeyValuePair = [string, REncoded]; - return { m: enc(values), k: sRandomId }; +/** + * decodeRValue + * 反编码:将已编码的数据恢复为真实值 + * + * @param rTyped 已编码的值 + * @returns 解码后的真实值(undefined | null | T) + * + * @example + * decodeRValue([RType.UNDEFINED]) // undefined + * decodeRValue([RType.NULL]) // null + * decodeRValue([RType.STANDARD, 123]) // 123 + */ +export const decodeRValue = (rTyped: REncoded) => { + switch (rTyped[0]) { + case RType.UNDEFINED: + return undefined; + case RType.NULL: + return null; + default: + return rTyped[1] as T; + } }; /** - * 将 encodeMessage 生成的编码数据还原为原始对象 - * @template T - * @param {TEncodedMessage} values - 编码后的消息对象 - * @returns {T} - 还原后的原始数据 - * @throws {Error} 当输入无效或格式错误时抛出异常 + * encodeRValue + * 编码:将普通值编码为 REncoded 元组,用于稳定传输与序列化。 + * + * @param value 原始值 + * @returns REncoded + * + * @example + * encodeRValue(undefined) // [RType.UNDEFINED] + * encodeRValue(null) // [RType.NULL] + * encodeRValue(123) // [RType.STANDARD, 123] */ -export const decodeMessage = (values: TEncodedMessage): T => { - const { m, k } = values; - if (m === null || m === undefined || !k || typeof k !== "string") throw new Error("invalid decodeMessage"); - - const sRandomId = k; - const sUndefined = `${sRandomId}undefined`; - const sNull = `${sRandomId}null`; - - /** - * 递归解码函数 - * 将占位符字符串还原为 undefined / null - * @param {any} input - 任意输入值 - * @returns {any} - 解码后的值 - */ - const dec = (input: any): any => { - if (input == sUndefined) return undefined; - if (input === sNull) return null; - if (typeof input !== "object") return input; - - // 数组递归处理 - if (Array.isArray(input)) { - return input.map(dec); - } - - // 对象递归处理 - const out: Record = {}; - for (const k in input) { - out[k] = dec(input[k]); - } - return out; - }; - - return dec(m); +export const encodeRValue = (value: T): REncoded => { + switch (value) { + case undefined: + return R_UNDEFINED as [RType.UNDEFINED]; + case null: + return R_NULL as [RType.NULL]; + default: + return [RType.STANDARD, value] as [RType.STANDARD, T]; + } }; From ce9797862e52fde6a1ff7ec504d5ae60a7587477 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Fri, 14 Nov 2025 10:26:34 +0900 Subject: [PATCH 3/9] =?UTF-8?q?=E4=BB=A3=E7=A0=81=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/service/service_worker/client.ts | 10 ++-- src/app/service/service_worker/gm_api.ts | 2 +- src/app/service/service_worker/value.ts | 51 +++++++++---------- src/pages/components/ScriptStorage/index.tsx | 4 +- .../components/UserConfigPanel/index.tsx | 16 ++++-- src/pages/import/App.tsx | 7 ++- 6 files changed, 50 insertions(+), 40 deletions(-) diff --git a/src/app/service/service_worker/client.ts b/src/app/service/service_worker/client.ts index b6416fc87..fa841ecf9 100644 --- a/src/app/service/service_worker/client.ts +++ b/src/app/service/service_worker/client.ts @@ -24,7 +24,8 @@ import type { GMInfoEnv } from "../content/types"; import { type SystemService } from "./system"; import { type ScriptInfo } from "@App/pkg/utils/scriptInstall"; import type { ScriptService, TCheckScriptUpdateOption, TOpenBatchUpdatePageOption } from "./script"; -import { type TKeyValuePair } from "@App/pkg/utils/message_value"; +import { encodeRValue, type TKeyValuePair } from "@App/pkg/utils/message_value"; +import { type TSetValuesParams } from "./value"; export class ServiceWorkerClient extends Client { constructor(msgSender: MessageSend) { @@ -235,11 +236,12 @@ export class ValueClient extends Client { return this.doThrow("getScriptValue", script); } - setScriptValue(params: { uuid: string; key: string; value: any }) { - return this.do("setScriptValue", params); + setScriptValue({ uuid, key, value, ts }: { uuid: string; key: string; value: any; ts?: number }) { + const keyValuePairs = [[key, encodeRValue(value)]] as TKeyValuePair[]; + return this.do("setScriptValues", { uuid, keyValuePairs, ts }); } - setScriptValues(params: { uuid: string; keyValuePairs: TKeyValuePair[] }) { + setScriptValues(params: TSetValuesParams) { return this.do("setScriptValues", params); } } diff --git a/src/app/service/service_worker/gm_api.ts b/src/app/service/service_worker/gm_api.ts index 6fc594f87..3f16fe674 100644 --- a/src/app/service/service_worker/gm_api.ts +++ b/src/app/service/service_worker/gm_api.ts @@ -352,7 +352,7 @@ export default class GMApi { runFlag: request.runFlag, tabId: sender.getSender()?.tab?.id || -1, }; - await this.value.setValues(request.script.uuid, id, keyValuePairs, valueSender, false); + await this.value.setValues({ uuid: request.script.uuid, id, keyValuePairs, isReplace: false, valueSender }); } @PermissionVerify.API() diff --git a/src/app/service/service_worker/value.ts b/src/app/service/service_worker/value.ts index c74cfb31f..8d0be8c5a 100644 --- a/src/app/service/service_worker/value.ts +++ b/src/app/service/service_worker/value.ts @@ -15,6 +15,15 @@ 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"; +export type TSetValuesParams = { + uuid: string; + id?: string; + keyValuePairs: TKeyValuePair[]; + isReplace: boolean; + ts?: number; + valueSender?: ValueUpdateSender; +}; + export class ValueService { logger: Logger; scriptDAO: ScriptDAO = new ScriptDAO(); @@ -151,13 +160,14 @@ export class ValueService { } // 批量设置 - async setValues( - uuid: string, - id: string, - keyValuePairs: TKeyValuePair[], - sender: ValueUpdateSender, - removeNotProvided: boolean - ) { + 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"); @@ -168,8 +178,8 @@ export class ValueService { const entries = [] as ValueUpdateDataREntry[]; const _flag = await stackAsyncTask(cacheKey, async () => { let valueModel: Value | undefined = await this.valueDAO.get(storageName); - const now = Date.now(); if (!valueModel) { + const now = Date.now(); const dataModel: { [key: string]: any } = {}; for (const [key, rTyped1] of keyValuePairs) { const value = decodeRValue(rTyped1); @@ -184,8 +194,8 @@ export class ValueService { uuid: uuid, storageName: storageName, data: dataModel, - createtime: now, - updatetime: now, + createtime: ts || Math.min(ts!, now), + updatetime: ts || Math.min(ts!, now), }; } else { let changed = false; @@ -206,7 +216,7 @@ export class ValueService { const rTyped2 = encodeRValue(oldValue); entries.push([key, rTyped1, rTyped2]); } - if (removeNotProvided) { + if (isReplace) { // 处理oldValue有但是没有在data.values中的情况 for (const key of Object.keys(oldValueRecord)) { if (!containedKeys.has(key)) { @@ -231,34 +241,21 @@ export class ValueService { entries: entries, uuid, storageName, - sender, + sender: valueSender, valueUpdated, } as ValueUpdateDataEncoded); // valueUpdate 消息用于 early script 的处理 this.mq.emit("valueUpdate", { script, valueUpdated }); } - setScriptValue({ uuid, key, value }: { uuid: string; key: string; value: any }, _sender: IGetSender) { - const valueSender = { - runFlag: "user", - tabId: -2, - }; - return this.setValue(uuid, "", key, value, valueSender); - } - - setScriptValues({ uuid, keyValuePairs }: { uuid: string; keyValuePairs: TKeyValuePair[] }, _sender: IGetSender) { - const valueSender = { - runFlag: "user", - tabId: -2, - }; - return this.setValues(uuid, "", keyValuePairs, valueSender, true); + setScriptValues(params: Pick, _sender: IGetSender) { + return this.setValues(params); } init(runtime: RuntimeService, popup: PopupService) { this.popup = popup; this.runtime = runtime; this.group.on("getScriptValue", this.getScriptValue.bind(this)); - this.group.on("setScriptValue", this.setScriptValue.bind(this)); this.group.on("setScriptValues", this.setScriptValues.bind(this)); this.mq.subscribe("deleteScripts", async (data) => { diff --git a/src/pages/components/ScriptStorage/index.tsx b/src/pages/components/ScriptStorage/index.tsx index edcfc8b09..bc891801b 100644 --- a/src/pages/components/ScriptStorage/index.tsx +++ b/src/pages/components/ScriptStorage/index.tsx @@ -35,7 +35,7 @@ const ScriptStorage: React.FC<{ // 保存单个键值 const saveData = (key: string, value: any) => { - valueClient.setScriptValue({ uuid: script!.uuid, key, value }); + valueClient.setScriptValue({ uuid: script!.uuid, key, value, ts: Date.now() }); const newRawData = { ...rawData, [key]: value }; if (value === undefined) { delete newRawData[key]; @@ -49,7 +49,7 @@ const ScriptStorage: React.FC<{ for (const [key, value] of Object.entries(newRawValue)) { keyValuePairs.push([key, encodeRValue(value)]); } - valueClient.setScriptValues({ uuid: script!.uuid, keyValuePairs }); + valueClient.setScriptValues({ uuid: script!.uuid, keyValuePairs, isReplace: true, ts: Date.now() }); updateRawData(newRawValue); }; diff --git a/src/pages/components/UserConfigPanel/index.tsx b/src/pages/components/UserConfigPanel/index.tsx index d22a82bd9..a4c89f8ae 100644 --- a/src/pages/components/UserConfigPanel/index.tsx +++ b/src/pages/components/UserConfigPanel/index.tsx @@ -18,6 +18,8 @@ import { import TabPane from "@arco-design/web-react/es/Tabs/tab-pane"; import { ValueClient } from "@App/app/service/service_worker/client"; import { message } from "@App/pages/store/global"; +import type { TKeyValuePair } from "@App/pkg/utils/message_value"; +import { encodeRValue } from "@App/pkg/utils/message_value"; const FormItem = Form.Item; @@ -58,18 +60,22 @@ const UserConfigPanel: React.FC<{ const saveValues = formRefs.current[tab].getFieldsValue(); // 更新value const valueClient = new ValueClient(message); + const uuid = script.uuid; + const keyValuePairs = [] as TKeyValuePair[]; for (const key of Object.keys(saveValues)) { for (const valueKey of Object.keys(saveValues[key])) { if (saveValues[key][valueKey] === undefined) { continue; } - valueClient.setScriptValue({ - uuid: script.uuid, - key: `${key}.${valueKey}`, - value: saveValues[key][valueKey], - }); + keyValuePairs.push([`${key}.${valueKey}`, encodeRValue(saveValues[key][valueKey])]); } } + valueClient.setScriptValues({ + uuid: uuid, + keyValuePairs: keyValuePairs, + isReplace: false, + ts: Date.now(), + }); Message.success(t("save_success")!); // 替换为键值对应的英文文本 setVisible(false); } diff --git a/src/pages/import/App.tsx b/src/pages/import/App.tsx index e74b3e4b2..7f7f4dc6a 100644 --- a/src/pages/import/App.tsx +++ b/src/pages/import/App.tsx @@ -10,6 +10,8 @@ import { CACHE_KEY_IMPORT_FILE } from "@App/app/cache_key"; import { parseBackupZipFile } from "@App/pkg/backup/utils"; import { scriptClient, synchronizeClient, valueClient } from "../store/features/script"; import { sleep } from "@App/pkg/utils/utils"; +import type { TKeyValuePair } from "@App/pkg/utils/message_value"; +import { encodeRValue } from "@App/pkg/utils/message_value"; const ScriptListItem = React.memo( ({ @@ -187,9 +189,12 @@ function App() { const entries = Object.entries(data); if (entries.length === 0) return; await sleep(((Math.random() * 600) | 0) + 200); + const uuid = item.script!.script.uuid!; + const keyValuePairs = [] as TKeyValuePair[]; for (const [key, value] of entries) { - await valueClient.setScriptValue(item.script!.script.uuid!, key, value); + keyValuePairs.push([key, encodeRValue(value)]); } + await valueClient.setScriptValues({ uuid: uuid, keyValuePairs, isReplace: false, ts: Date.now() }); })(), ]); setInstallNum((prev) => [prev[0] + 1, prev[1]]); From 1aee2542a0b596fe86e2a40e36036246d259a20a Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Fri, 14 Nov 2025 11:38:34 +0900 Subject: [PATCH 4/9] =?UTF-8?q?=E8=AA=BF=E6=95=B4=20utils?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pkg/utils/async_queue.test.ts | 12 +----------- src/pkg/utils/utils.test.ts | 25 ++++++++++++++++++++++++- src/pkg/utils/utils.ts | 28 ++++++++++++++++++++++++++++ 3 files changed, 53 insertions(+), 12 deletions(-) diff --git a/src/pkg/utils/async_queue.test.ts b/src/pkg/utils/async_queue.test.ts index 01675eafb..67730708a 100644 --- a/src/pkg/utils/async_queue.test.ts +++ b/src/pkg/utils/async_queue.test.ts @@ -1,22 +1,12 @@ // async_queue.test.ts import { describe, it, expect } from "vitest"; import { stackAsyncTask } from "./async_queue"; +import { deferred } from "@App/pkg/utils/utils"; /* ==================== 工具函数 ==================== */ const generateKey = (prefix: string) => `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2)}`; -/** 手动控制的 Promise(用于阻塞) */ -const deferred = () => { - let resolve!: (v: T | PromiseLike) => void; - let reject!: (e?: any) => void; - const promise = new Promise((res, rej) => { - resolve = res; - reject = rej; - }); - return { promise, resolve, reject }; -}; - const nextTick = () => Promise.resolve(); /** 强制执行所有已入队微任务与 then 链 */ const flush = async () => { diff --git a/src/pkg/utils/utils.test.ts b/src/pkg/utils/utils.test.ts index 3289b6dc4..e255abdd1 100644 --- a/src/pkg/utils/utils.test.ts +++ b/src/pkg/utils/utils.test.ts @@ -1,9 +1,32 @@ import { describe, expect, it, beforeAll } from "vitest"; -import { checkSilenceUpdate, cleanFileName, stringMatching, toCamelCase } from "./utils"; +import { aNow, checkSilenceUpdate, cleanFileName, stringMatching, toCamelCase } from "./utils"; import { ltever, versionCompare } from "@App/pkg/utils/semver"; import { nextTime } from "./cron"; import dayjs from "dayjs"; +describe.concurrent("aNow", () => { + it.sequential("aNow is Strictly Increasing", () => { + const p1 = [aNow(), aNow(), aNow(), aNow(), aNow(), aNow()]; + expect(p1[0]).lessThan(p1[1]); + expect(p1[1]).lessThan(p1[2]); + expect(p1[2]).lessThan(p1[3]); + expect(p1[3]).lessThan(p1[4]); + expect(p1[4]).lessThan(p1[5]); + const p2 = [...p1].sort(); + expect(p1).toEqual(p2); + }); + it.sequential("t1 > t2 (busy) and t3 = t4 (idle)", async () => { + const _p1 = [aNow(), aNow(), aNow(), aNow(), aNow(), aNow()]; + const t1 = aNow(); + const t2 = Date.now(); + expect(t1).greaterThan(t2); + await new Promise((resolve) => setTimeout(resolve, 10)); + const t3 = aNow(); + const t4 = Date.now(); + expect(t3).toEqual(t4); + }); +}); + describe.concurrent("nextTime", () => { const date = new Date(1737275107000); // 让程序先执行一下,避免超时问题 diff --git a/src/pkg/utils/utils.ts b/src/pkg/utils/utils.ts index 555f72011..62e4acf75 100644 --- a/src/pkg/utils/utils.ts +++ b/src/pkg/utils/utils.ts @@ -11,6 +11,34 @@ export function randomMessageFlag(): string { return `-${Date.now().toString(36)}.${randNum(8e11, 2e12).toString(36)}`; } +let prevNow = 0; +/** + * accumulated "now". + * 用 aNow 取得的现在时间能保证严格递增 + */ +export const aNow = () => { + let now = Date.now(); + if (prevNow >= now) now = prevNow + 0.0009765625; // 2^-10 + prevNow = now; + return now; +}; + +export type Deferred = { + promise: Promise; + resolve: (v: T | PromiseLike) => void; + reject: (e?: any) => void; +}; + +export const deferred = (): Deferred => { + let resolve!: (v: T | PromiseLike) => void; + let reject!: (e?: any) => void; + const promise = new Promise((res, rej) => { + resolve = res; + reject = rej; + }); + return { promise, resolve, reject }; +}; + export function isFirefox() { //@ts-ignore return typeof mozInnerScreenX !== "undefined"; From 19e76653de71032e8078448059ce224704b6ec4f Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Fri, 14 Nov 2025 11:42:51 +0900 Subject: [PATCH 5/9] =?UTF-8?q?=E4=BB=A3=E7=A2=BC=E8=AA=BF=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/service/service_worker/gm_api.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/app/service/service_worker/gm_api.ts b/src/app/service/service_worker/gm_api.ts index 3f16fe674..e2c763138 100644 --- a/src/app/service/service_worker/gm_api.ts +++ b/src/app/service/service_worker/gm_api.ts @@ -30,7 +30,7 @@ import type { import type { TScriptMenuRegister, TScriptMenuUnregister } from "../queue"; import { BrowserNoSupport, notificationsUpdate } from "./utils"; import i18n from "@App/locales/locales"; -import { type TKeyValuePair } from "@App/pkg/utils/message_value"; +import { encodeRValue, type TKeyValuePair } from "@App/pkg/utils/message_value"; import { createObjectURL } from "../offscreen/client"; // GMApi,处理脚本的GM API调用请求 @@ -336,10 +336,12 @@ export default class GMApi { throw new Error("param is failed"); } const [id, key, value] = request.params as [string, string, any]; - await this.value.setValue(request.script.uuid, id, key, value, { + const keyValuePairs = [[key, encodeRValue(value)]] as TKeyValuePair[]; + const valueSender = { runFlag: request.runFlag, tabId: sender.getSender()?.tab?.id || -1, - }); + }; + await this.value.setValues({ uuid: request.script.uuid, id, keyValuePairs, isReplace: false, valueSender }); } @PermissionVerify.API({ link: ["GM_deleteValues"] }) From 129005a794587312a24c2dc8db5694f2f64a5e4b Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Fri, 14 Nov 2025 11:50:06 +0900 Subject: [PATCH 6/9] Update value.test.ts --- src/app/service/service_worker/value.test.ts | 72 +++++++++++++++++--- 1 file changed, 64 insertions(+), 8 deletions(-) diff --git a/src/app/service/service_worker/value.test.ts b/src/app/service/service_worker/value.test.ts index 685a1bfde..bd28eaaec 100644 --- a/src/app/service/service_worker/value.test.ts +++ b/src/app/service/service_worker/value.test.ts @@ -14,6 +14,8 @@ 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"; initTestEnv(); @@ -104,7 +106,13 @@ describe("ValueService - setValue 方法测试", () => { vi.mocked(mockValueDAO.save).mockResolvedValue({} as any); // 执行测试 - await valueService.setValue(mockScript.uuid, "testId-4021", key, value, mockSender); + await valueService.setValues({ + uuid: mockScript.uuid, + id: "testId-4021", + keyValuePairs: [[key, encodeRValue(value)]], + valueSender: mockSender, + isReplace: false, + }); // 验证结果 expect(mockScriptDAO.get).toHaveBeenCalledWith(mockScript.uuid); @@ -151,7 +159,13 @@ describe("ValueService - setValue 方法测试", () => { vi.mocked(mockValueDAO.save).mockResolvedValue({} as any); // 执行测试 - await valueService.setValue(mockScript.uuid, "testId-4022", key, value, mockSender); + await valueService.setValues({ + uuid: mockScript.uuid, + id: "testId-4022", + keyValuePairs: [[key, encodeRValue(value)]], + valueSender: mockSender, + isReplace: false, + }); // 验证结果 expect(mockScriptDAO.get).toHaveBeenCalledWith(mockScript.uuid); @@ -207,7 +221,13 @@ describe("ValueService - setValue 方法测试", () => { vi.mocked(mockValueDAO.save).mockResolvedValue({} as any); // 执行测试 - await valueService.setValue(mockScript.uuid, "testId-4023", key, newValue, mockSender); + await valueService.setValues({ + uuid: mockScript.uuid, + id: "testId-4023", + keyValuePairs: [[key, encodeRValue(newValue)]], + valueSender: mockSender, + isReplace: false, + }); // 验证结果 expect(mockScriptDAO.get).toHaveBeenCalledWith(mockScript.uuid); @@ -262,7 +282,13 @@ describe("ValueService - setValue 方法测试", () => { vi.mocked(mockValueDAO.get).mockResolvedValue(existingValueModel); // 执行测试 - await valueService.setValue(mockScript.uuid, "testId-4024", key, value, mockSender); + await valueService.setValues({ + uuid: mockScript.uuid, + id: "testId-4024", + keyValuePairs: [[key, encodeRValue(value)]], + valueSender: mockSender, + isReplace: false, + }); // 验证结果 - 不应该保存或发送更新 expect(mockScriptDAO.get).toHaveBeenCalledWith(mockScript.uuid); @@ -308,7 +334,13 @@ describe("ValueService - setValue 方法测试", () => { vi.mocked(mockValueDAO.save).mockResolvedValue({} as any); // 执行测试 - 设置值为undefined - await valueService.setValue(mockScript.uuid, "testId-4025", key, undefined, mockSender); + await valueService.setValues({ + uuid: mockScript.uuid, + id: "testId-4025", + keyValuePairs: [[key, encodeRValue(undefined)]], + valueSender: mockSender, + isReplace: false, + }); // 验证结果 expect(mockValueDAO.save).toHaveBeenCalled(); @@ -329,8 +361,15 @@ describe("ValueService - setValue 方法测试", () => { vi.mocked(mockScriptDAO.get).mockResolvedValue(undefined); // 执行测试并验证抛出错误 + const keyValuePairs1 = [["testKey", encodeRValue("testValue")]] satisfies TKeyValuePair[]; await expect( - valueService.setValue(nonExistentUuid, "testId-4026", "testKey", "testValue", mockSender) + valueService.setValues({ + uuid: nonExistentUuid, + id: "testId-4026", + keyValuePairs: keyValuePairs1, + valueSender: mockSender, + isReplace: false, + }) ).rejects.toThrow("script not found"); // 验证不会执行后续操作 @@ -353,11 +392,28 @@ describe("ValueService - setValue 方法测试", () => { vi.mocked(mockScriptDAO.get).mockResolvedValue(mockScript); vi.mocked(mockValueDAO.get).mockResolvedValue(undefined); vi.mocked(mockValueDAO.save).mockResolvedValue({} as any); + expect(mockScriptDAO.get).toHaveBeenCalledTimes(0); + expect(mockValueDAO.save).toHaveBeenCalledTimes(0); + expect(valueService.pushValueToTab).toHaveBeenCalledTimes(0); // 并发执行两个setValue操作 + const keyValuePairs1 = [[key1, encodeRValue(value1)]] satisfies TKeyValuePair[]; + const keyValuePairs2 = [[key2, encodeRValue(value2)]] satisfies TKeyValuePair[]; await Promise.all([ - valueService.setValue(mockScript.uuid, "testId-4041", key1, value1, mockSender), - valueService.setValue(mockScript.uuid, "testId-4042", key2, value2, mockSender), + valueService.setValues({ + uuid: mockScript.uuid, + id: "testId-4041", + keyValuePairs: keyValuePairs1, + valueSender: mockSender, + isReplace: false, + }), + valueService.setValues({ + uuid: mockScript.uuid, + id: "testId-4042", + keyValuePairs: keyValuePairs2, + valueSender: mockSender, + isReplace: false, + }), ]); // 验证两个操作都被调用 From f04aed202d9e4678b6bb85cd9205a8b9804a5c30 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Fri, 14 Nov 2025 11:55:42 +0900 Subject: [PATCH 7/9] remove unused setValue --- src/app/service/service_worker/value.ts | 52 ------------------------- 1 file changed, 52 deletions(-) diff --git a/src/app/service/service_worker/value.ts b/src/app/service/service_worker/value.ts index 8d0be8c5a..802b2a657 100644 --- a/src/app/service/service_worker/value.ts +++ b/src/app/service/service_worker/value.ts @@ -71,58 +71,6 @@ export class ValueService { return newValues; } - async setValue(uuid: string, id: string, key: string, value: any, sender: ValueUpdateSender) { - // 查询出脚本 - const script = await this.scriptDAO.get(uuid); - if (!script) { - throw new Error("script not found"); - } - // 查询老的值 - const storageName = getStorageName(script); - let oldValue; - // 使用事务来保证数据一致性 - const cacheKey = `${CACHE_KEY_SET_VALUE}${storageName}`; - const valueUpdated = await stackAsyncTask(cacheKey, async () => { - let valueModel: Value | undefined = await this.valueDAO.get(storageName); - if (!valueModel) { - const now = Date.now(); - valueModel = { - uuid: script.uuid, - storageName: storageName, - data: { [key]: value }, - createtime: now, - updatetime: now, - }; - } else { - let dataModel = valueModel.data; - // 值没有发生变化, 不进行操作 - oldValue = dataModel[key]; - if (oldValue === value) { - return false; - } - dataModel = { ...dataModel }; // 每次储存使用新参考 - if (value === undefined) { - delete dataModel[key]; - } else { - dataModel[key] = value; - } - valueModel.data = dataModel; // 每次储存使用新参考 - } - await this.valueDAO.save(storageName, valueModel); - return true; - }); - this.pushValueToTab({ - id, - entries: [[key, encodeRValue(value), encodeRValue(oldValue)]], - uuid, - storageName, - sender, - valueUpdated, - } satisfies ValueUpdateDataEncoded); - // valueUpdate 消息用于 early script 的处理 - this.mq.emit("valueUpdate", { script, valueUpdated }); - } - // 推送值到tab async pushValueToTab(sendData: T) { const { storageName } = sendData; From 74a1837dc13bb3d21122aaf7f0f882b0b917abbb Mon Sep 17 00:00:00 2001 From: wangyizhi Date: Sat, 15 Nov 2025 21:57:10 +0800 Subject: [PATCH 8/9] Update src/app/service/service_worker/value.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/app/service/service_worker/value.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/service/service_worker/value.ts b/src/app/service/service_worker/value.ts index 802b2a657..ceac73e18 100644 --- a/src/app/service/service_worker/value.ts +++ b/src/app/service/service_worker/value.ts @@ -142,8 +142,8 @@ export class ValueService { uuid: uuid, storageName: storageName, data: dataModel, - createtime: ts || Math.min(ts!, now), - updatetime: ts || Math.min(ts!, now), + createtime: ts || now, + updatetime: ts || now, }; } else { let changed = false; From f59c79272b96f653ede86730495a79ea391f343e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E4=B8=80=E4=B9=8B?= Date: Sat, 15 Nov 2025 21:57:22 +0800 Subject: [PATCH 9/9] =?UTF-8?q?=E4=BC=98=E5=8C=96=E4=B8=80=E4=B8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/service/service_worker/client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/service/service_worker/client.ts b/src/app/service/service_worker/client.ts index db45b8576..1013fe130 100644 --- a/src/app/service/service_worker/client.ts +++ b/src/app/service/service_worker/client.ts @@ -238,7 +238,7 @@ export class ValueClient extends Client { setScriptValue({ uuid, key, value, ts }: { uuid: string; key: string; value: any; ts?: number }) { const keyValuePairs = [[key, encodeRValue(value)]] as TKeyValuePair[]; - return this.do("setScriptValues", { uuid, keyValuePairs, ts }); + return this.do("setScriptValues", { uuid, keyValuePairs, ts } as TSetValuesParams); } setScriptValues(params: TSetValuesParams) {