Conversation
|
OPFS确实适合存储这些信息,有的脚本,一个脚本好几M;不过这个改动先放一下,先处理一下其它的内容(可能1.5?) |
可以在 1.4 先升级成过渡版本 |
|
你这过度的方式不太对劲吧,应该还是 ScriptCodeDAO,只要改ScriptCodeDAO内部的处理,先读OPFS,没有再读chrome.local.storage,这首次启动直接爆炸了 |
There was a problem hiding this comment.
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);
src/app/migrate.ts
Outdated
| const scriptCodeDAO = new ScriptCodeDAO(); | ||
| const scriptCodeDAONew = new ScriptCodeDAONew(); | ||
| const scriptCodes = await scriptCodeDAO.all(); | ||
| await Promise.all(scriptCodes.map(async (scriptCode) => scriptCodeDAONew.save(scriptCode))); |
There was a problem hiding this comment.
migrate v2 这里用 ScriptCodeDAONew.save() 迁移旧 ScriptCodeDAO 数据到 OPFS,但 ScriptCodeDAONew.save() 内部又会“过渡期间同步保存至 ScriptCodeDAO”,等于迁移过程中把所有 code 再写回一次 chrome.storage.local,仍会触发大量 onChanged/写放大。建议为迁移提供仅写 OPFS 的路径(例如 save({..., legacy: false}) 或独立的 saveToOPFSOnly)。
| 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)) | |
| ); |
src/app/migrate.ts
Outdated
| await m.upgrade(); | ||
| migrations.push(m.version); | ||
| migrationVersion = m.version; | ||
| localstorageDAO.save({ |
There was a problem hiding this comment.
migrateChromeStorage 在升级循环内调用 localstorageDAO.save(...) 但未 await;如果 service worker 在写入完成前被挂起/回收,可能导致迁移版本号未落盘,从而重复执行迁移(尤其是 v2 这种全量迁移)。建议对 save 返回的 Promise 做 await(或把整个 migrateChromeStorage 改为 async 并在调用处等待)。
| localstorageDAO.save({ | |
| await localstorageDAO.save({ |
src/app/repo/scripts.ts
Outdated
| 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()); |
There was a problem hiding this comment.
ScriptCodeDAONew.get() 在文件不存在时返回 { uuid, code: "" }(始终为 truthy),会导致调用方用 if (!scriptCode) / if (!code) 判断“代码缺失”永远不会触发,从而把“缺失”当成“空代码”继续执行。建议让 get() 在文件不存在时返回 undefined(并把返回类型改为 Promise<ScriptCode | undefined>),或提供一个明确的 getOrEmpty() 并统一调用方语义。
| 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()); |
src/app/repo/scripts.ts
Outdated
| } | ||
| } | ||
|
|
||
| // 不能 extends Repo<ScriptCode>. 没有 dao.gets() |
There was a problem hiding this comment.
这里的注释“不能 extends Repo. 没有 dao.gets()”与当前 Repo 实现不一致:Repo 本身就有 gets()。建议更新为真实原因(例如:此 DAO 不走 chrome.storage.local 的 Repo 前缀/缓存逻辑,而是走 OPFS),避免误导后续维护。
| // 不能 extends Repo<ScriptCode>. 没有 dao.gets() | |
| // 注意:此 DAO 不继承 Repo<ScriptCode>,而是改用 OPFS 单独存储大体积脚本代码, |
| } | ||
| const scriptCode = await new ScriptCodeDAO().get(old.uuid); | ||
| const scriptCode = await new ScriptCodeDAONew().get(old.uuid); | ||
| if (!scriptCode) { |
There was a problem hiding this comment.
prepareScriptByCode 这里仍用 if (!scriptCode) 判断旧脚本 code 是否存在,但 ScriptCodeDAONew.get() 当前不会返回 undefined(缺文件时返回空字符串对象),会导致缺失 code 的情况不再抛 error_old_script_code_missing,而是继续用空 code 走后续逻辑。建议配合调整 ScriptCodeDAONew.get() 的返回语义,或在这里改为检查 code.code 是否为空。
| if (!scriptCode) { | |
| // ScriptCodeDAONew.get() 在缺文件时可能返回空字符串对象,此处需同时检查 code 字段 | |
| if (!scriptCode || !scriptCode.code) { |
src/app/migrate.ts
Outdated
| ]; | ||
| const localstorageDAO = new LocalStorageDAO(); | ||
| localstorageDAO.get("migrations").then(async (item) => { | ||
| // 旧代码 logic 错误。只好维持 array 形式但只储单一版本号 |
There was a problem hiding this comment.
哪里有错误?如果是顺序,也不应该这样去处理,应该保留完整版本
There was a problem hiding this comment.
单一版本号升级 1 -> 2 -> 3 -> 4...
不是一个阵列
如果有20个 migration 动作,就会储存20个数字
- ScriptCodeDAO 不再继承 Repo,过渡期间的 chrome.storage.local 操作 改为 legacySave/legacyGet/legacyDelete 内部方法,过渡结束后直接删除 - save() 增加 try-catch,OPFS 写入失败不影响 chrome.storage.local - OPFS mock 从 vitest.setup.ts 抽离到 tests/opfs_mock.ts
|
先转 draft 看看改动有没有什么问题 |

注意!升级后,原本的ScriptCode DAO不再使用。过渡期间保留旧 ScriptCodeDAO 数据。必须 STABLE 和 BETA 都移植到 ScriptCodeDAONew 才删掉过渡期间的代码。
注意!在未使用 ScriptCodeDAONew 的 STABLE版本进行代码更改的话,不会在 BETA版本的 ScriptCodeDAONew 反映
Checklist / 检查清单
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 / 截图