Skip to content

Breaking: 以 OPFS 储存 Code#1316

Draft
cyfung1031 wants to merge 9 commits intorelease/v1.4from
breaking/ScriptCodeDAONew
Draft

Breaking: 以 OPFS 储存 Code#1316
cyfung1031 wants to merge 9 commits intorelease/v1.4from
breaking/ScriptCodeDAONew

Conversation

@cyfung1031
Copy link
Copy Markdown
Collaborator

@cyfung1031 cyfung1031 commented Mar 28, 2026

注意!升级后,原本的ScriptCode DAO不再使用。过渡期间保留旧 ScriptCodeDAO 数据。必须 STABLE 和 BETA 都移植到 ScriptCodeDAONew 才删掉过渡期间的代码。

注意!在未使用 ScriptCodeDAONew 的 STABLE版本进行代码更改的话,不会在 BETA版本的 ScriptCodeDAONew 反映


Checklist / 检查清单

  • Fixes mentioned issues / 修复已提及的问题
  • Code reviewed by human / 代码通过人工检查
  • Changes tested / 已完成测试

Description / 描述

使用 chrome.storage.local 进行 Code的储存是错误的选择。每个脚本可以动不动就2 ~ 3MB。30个就已经可以接近 100MB
1)进行 storage 操作会触发 onChanged, 这个资料传递量太大了
改用OPFS后,有需要才拿出来
一般都已经用 userScript Register 储存好了。只有 editor 时才需要拿出来
2)目前需要使用 chrome.storage.local.get() 的 cache。Code放在 chrome.storage.local 的话,这个初始化行为会一次过取 数百 MB,不理想。 而且一直放在JS cache 物件里

Screenshots / 截图

@cyfung1031 cyfung1031 requested a review from CodFrm March 28, 2026 10:46
@cyfung1031
Copy link
Copy Markdown
Collaborator Author

我有18个脚本

await chrome.storage.local.get()

Object.fromEntries(Object.entries(await chrome.storage.local.get()).filter(e=>!e[0].startsWith("scriptCode:")))

[
    await chrome.storage.local.get(),
    Object.fromEntries(Object.entries(await chrome.storage.local.get())
        .filter(e => !e[0].startsWith("scriptCode:")))
].map(x => new TextEncoder().encode(JSON.stringify(x)).length);
Screenshot 2026-03-28 at 19 57 50

[1798744, 296294]

296294 bytes / 1798744 bytes = 0.165

拿走代码后 chrome.storage.local 的大小减少了 83.5%

@cyfung1031 cyfung1031 added the breaking Breaking Change label Mar 28, 2026
@CodFrm
Copy link
Copy Markdown
Member

CodFrm commented Mar 28, 2026

OPFS确实适合存储这些信息,有的脚本,一个脚本好几M;不过这个改动先放一下,先处理一下其它的内容(可能1.5?)

@cyfung1031
Copy link
Copy Markdown
Collaborator Author

OPFS确实适合存储这些信息,有的脚本,一个脚本好几M;不过这个改动先放一下,先处理一下其它的内容(可能1.5?)

可以在 1.4 先升级成过渡版本
1.5 再升级成正式

@CodFrm
Copy link
Copy Markdown
Member

CodFrm commented Mar 29, 2026

你这过度的方式不太对劲吧,应该还是 ScriptCodeDAO,只要改ScriptCodeDAO内部的处理,先读OPFS,没有再读chrome.local.storage,这首次启动直接爆炸了

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

该 PR 将脚本代码(Code)的存储从 chrome.storage.local 逐步迁移到 OPFS(Origin Private File System),以减少大体积脚本代码引发的 onChanged 事件开销与初始化全量加载压力,并在过渡期保留旧 ScriptCodeDAO 数据。

Changes:

  • 新增 ScriptCodeDAONew,用 OPFS 读写脚本 code,并在过渡期同步写入旧 ScriptCodeDAO
  • 将多处读取脚本 code 的调用从 ScriptCodeDAO 切换为 ScriptCodeDAONew.get()
  • 增加 chrome.storage 迁移步骤(v2)将旧 code 全量写入 OPFS,并调整迁移版本记录逻辑

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
src/pkg/utils/script.ts 更新导入/安装流程读取旧脚本 code 的 DAO 实现
src/pages/options/routes/script/ScriptEditor.tsx 编辑器打开脚本时改用新 DAO 读取 code
src/pages/components/CloudScriptPlan/index.tsx 导出流程改用新 DAO 获取 code
src/app/service/service_worker/synchronize.ts 同步服务改用新 DAO 实例(字段注入方式调整)
src/app/service/service_worker/subscribe.ts 订阅删除脚本查询方式从 findByUUID 改为 get
src/app/service/service_worker/script.ts 脚本服务改用新 DAO,并移除/停用旧缓存启用调用
src/app/service/service_worker/runtime.ts 运行时加载脚本元信息时改从新 DAO 取 code
src/app/repo/scripts.ts 新增 ScriptCodeDAONew(OPFS 存储实现),并调整 ScriptDAO 结构
src/app/migrate.ts 新增 migration v2:从旧 code 存储迁移到 OPFS,并调整 migration 版本记录
Comments suppressed due to low confidence (3)

src/pages/components/CloudScriptPlan/index.tsx:130

  • 这里 const code = await new ScriptCodeDAONew().get(...) 后用 if (!code) 判断无效:按当前实现 get() 始终返回对象,可能导致“脚本代码缺失”时仍继续导出空内容。建议改为基于返回值为 undefined(推荐)或检查 code.code 是否为空来决定报错。
          const code = await new ScriptCodeDAONew().get(script.uuid);
          if (!code) {
            Message.error(t("invalid_script_code"));
            return;
          }

src/pages/options/routes/script/ScriptEditor.tsx:280

  • ScriptEditor 打开脚本时直接用 get() 读取 code;在 ScriptCodeDAONew.get() 缺文件返回空 code 的语义下,会静默把“缺失脚本代码”当成空白代码载入编辑器(code?.code || "")。建议让 get() 返回 undefined 表示缺失并在此处给出明确错误提示/阻止打开。
          const code = await scriptCodeDAO.get(uuid);
          const newEditor: EditorState = {
            script,
            code: code?.code || "",
            active: true,

src/app/service/service_worker/runtime.ts:1273

  • 这里仍保留 if (code) 分支,但在 ScriptCodeDAONew.get() 当前实现下(缺文件也返回对象),该判断将恒为 true,可能把缺失 code 的脚本 metadataStr/userConfigStr 置为空字符串并掩盖数据问题。建议配合让 get() 返回 undefined 表示缺失,并据此跳过/上报缺失脚本代码。
        scriptCodeDAO.get(script.uuid).then((code) => {
          if (code) {
            const metadataStr = getMetadataStr(code.code) || "";
            const userConfigStr = getUserConfigStr(code.code) || "";
            const userConfig = parseUserConfig(userConfigStr);

const scriptCodeDAO = new ScriptCodeDAO();
const scriptCodeDAONew = new ScriptCodeDAONew();
const scriptCodes = await scriptCodeDAO.all();
await Promise.all(scriptCodes.map(async (scriptCode) => scriptCodeDAONew.save(scriptCode)));
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

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

migrate v2 这里用 ScriptCodeDAONew.save() 迁移旧 ScriptCodeDAO 数据到 OPFS,但 ScriptCodeDAONew.save() 内部又会“过渡期间同步保存至 ScriptCodeDAO”,等于迁移过程中把所有 code 再写回一次 chrome.storage.local,仍会触发大量 onChanged/写放大。建议为迁移提供仅写 OPFS 的路径(例如 save({..., legacy: false}) 或独立的 saveToOPFSOnly)。

Suggested change
await Promise.all(scriptCodes.map(async (scriptCode) => scriptCodeDAONew.save(scriptCode)));
// 迁移阶段仅写入 OPFS,避免通过 ScriptCodeDAONew.save 再次写回 chrome.storage.local 造成写放大
await Promise.all(
scriptCodes.map(async (scriptCode) => scriptCodeDAONew.saveToOPFSOnly(scriptCode))
);

Copilot uses AI. Check for mistakes.
await m.upgrade();
migrations.push(m.version);
migrationVersion = m.version;
localstorageDAO.save({
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

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

migrateChromeStorage 在升级循环内调用 localstorageDAO.save(...) 但未 await;如果 service worker 在写入完成前被挂起/回收,可能导致迁移版本号未落盘,从而重复执行迁移(尤其是 v2 这种全量迁移)。建议对 save 返回的 Promise 做 await(或把整个 migrateChromeStorage 改为 async 并在调用处等待)。

Suggested change
localstorageDAO.save({
await localstorageDAO.save({

Copilot uses AI. Check for mistakes.
Comment on lines +319 to +333
public async get(uuid: string): Promise<ScriptCode> {
if (!this._dirHandlePromise) this._dirHandlePromise = ScriptCodeDAONew.getDirHandle();
const folder = await this._dirHandlePromise;
let code: string = "";
let handle: FileSystemFileHandle;
try {
handle = await folder.getFileHandle(`${uuid}.user.js`, { create: false });
} catch {
// no file -> empty code
return {
uuid,
code,
};
}
code = await handle.getFile().then((f) => f.text());
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

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

ScriptCodeDAONew.get() 在文件不存在时返回 { uuid, code: "" }(始终为 truthy),会导致调用方用 if (!scriptCode) / if (!code) 判断“代码缺失”永远不会触发,从而把“缺失”当成“空代码”继续执行。建议让 get() 在文件不存在时返回 undefined(并把返回类型改为 Promise<ScriptCode | undefined>),或提供一个明确的 getOrEmpty() 并统一调用方语义。

Suggested change
public async get(uuid: string): Promise<ScriptCode> {
if (!this._dirHandlePromise) this._dirHandlePromise = ScriptCodeDAONew.getDirHandle();
const folder = await this._dirHandlePromise;
let code: string = "";
let handle: FileSystemFileHandle;
try {
handle = await folder.getFileHandle(`${uuid}.user.js`, { create: false });
} catch {
// no file -> empty code
return {
uuid,
code,
};
}
code = await handle.getFile().then((f) => f.text());
public async get(uuid: string): Promise<ScriptCode | undefined> {
if (!this._dirHandlePromise) this._dirHandlePromise = ScriptCodeDAONew.getDirHandle();
const folder = await this._dirHandlePromise;
let handle: FileSystemFileHandle;
try {
handle = await folder.getFileHandle(`${uuid}.user.js`, { create: false });
} catch {
// no file -> undefined
return undefined;
}
const code = await handle.getFile().then((f) => f.text());

Copilot uses AI. Check for mistakes.
}
}

// 不能 extends Repo<ScriptCode>. 没有 dao.gets()
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

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

这里的注释“不能 extends Repo. 没有 dao.gets()”与当前 Repo 实现不一致:Repo 本身就有 gets()。建议更新为真实原因(例如:此 DAO 不走 chrome.storage.local 的 Repo 前缀/缓存逻辑,而是走 OPFS),避免误导后续维护。

Suggested change
// 不能 extends Repo<ScriptCode>. 没有 dao.gets()
// 注意:此 DAO 不继承 Repo<ScriptCode>,而是改用 OPFS 单独存储大体积脚本代码,

Copilot uses AI. Check for mistakes.
}
const scriptCode = await new ScriptCodeDAO().get(old.uuid);
const scriptCode = await new ScriptCodeDAONew().get(old.uuid);
if (!scriptCode) {
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

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

prepareScriptByCode 这里仍用 if (!scriptCode) 判断旧脚本 code 是否存在,但 ScriptCodeDAONew.get() 当前不会返回 undefined(缺文件时返回空字符串对象),会导致缺失 code 的情况不再抛 error_old_script_code_missing,而是继续用空 code 走后续逻辑。建议配合调整 ScriptCodeDAONew.get() 的返回语义,或在这里改为检查 code.code 是否为空。

Suggested change
if (!scriptCode) {
// ScriptCodeDAONew.get() 在缺文件时可能返回空字符串对象,此处需同时检查 code 字段
if (!scriptCode || !scriptCode.code) {

Copilot uses AI. Check for mistakes.
];
const localstorageDAO = new LocalStorageDAO();
localstorageDAO.get("migrations").then(async (item) => {
// 旧代码 logic 错误。只好维持 array 形式但只储单一版本号
Copy link
Copy Markdown
Member

@CodFrm CodFrm Mar 29, 2026

Choose a reason for hiding this comment

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

哪里有错误?如果是顺序,也不应该这样去处理,应该保留完整版本

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

单一版本号升级 1 -> 2 -> 3 -> 4...
不是一个阵列
如果有20个 migration 动作,就会储存20个数字

CodFrm added 5 commits March 29, 2026 20:46
- ScriptCodeDAO 不再继承 Repo,过渡期间的 chrome.storage.local 操作
  改为 legacySave/legacyGet/legacyDelete 内部方法,过渡结束后直接删除
- save() 增加 try-catch,OPFS 写入失败不影响 chrome.storage.local
- OPFS mock 从 vitest.setup.ts 抽离到 tests/opfs_mock.ts
@cyfung1031
Copy link
Copy Markdown
Collaborator Author

先转 draft 看看改动有没有什么问题

@cyfung1031 cyfung1031 marked this pull request as draft March 29, 2026 22:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking Breaking Change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants