✨ 为 GM.download/GM_download 添加 onloadend 回调#1563
Draft
cyfung1031 wants to merge 5 commits into
Draft
Conversation
对齐 GM_xmlhttpRequest 的 onloadend 语义:无论下载以 onload/save_cancelled/ ontimeout/onerror 中的哪一种结束,onloadend 都会紧随其后触发一次,覆盖 browser 与 native 两种 downloadMode 的所有终态分支。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- native 模式下 xhr 因 ontimeout/onerror 失败后,底层实现仍会在其 onloadend 带上已收数据拼出的 Blob;补充 xhrFailed 标志位拦截,避免把部分数据当作 下载成功经 chrome.downloads 落盘为被截断的文件,且避免用户 onloadend 被 触发两次。 - browser 模式下 a.connect() 失败此前未被捕获,导致 GM.download 返回的 promise 永远 pending;补上与 native 模式一致的 try/catch,通过 onerror/ onloadend/reject 通知调用方。 - 所有终态分支(onload/save_cancelled/ontimeout/onerror/default)新增 try/finally,防止用户 onloadend 抛错时跳过 retPromiseResolve/Reject 与 releaseResources(避免 promise 悬挂、blob URL 泄漏)。 - 补上 src/template/scriptcat.d.tpl 中 DownloadDetails.onloadend 声明, 修复内置编辑器仍将 onloadend 报告为未知属性的问题。 新增 6 个回归测试覆盖上述场景。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This comment was marked as outdated.
This comment was marked as outdated.
- 新增 withLoadEnd 共享辅助函数,将「主回调」(onload/onerror/ontimeout) 与 onloadend 分别置于内层 try/finally:此前两者共享同一个 try 区块,一旦 主回调抛错会直接跳到外层 finally,导致 onloadend 从未被调用,违反其 “总是紧随主回调之后触发”的约定。该辅助函数应用于 browser 模式、native 模式浏览器下载阶段的所有终态分支,以及 native 模式 xhr 阶段的 ontimeout/onerror。 - native 模式 xhr 阶段的 ontimeout/onerror 新增无条件的 retPromiseReject 调用(置于 finally 内,不再依赖 GM_xmlhttpRequest 内部 retPromise 延后 reject 再经由 `if (aborted) return` 转发)。此前若使用者在 onloadend 回调内呼叫 download.abort(),aborted 旗标会被提前设为 true,导致延后的 reject 被短路跳过,GM.download 返回的 promise 永久 pending。Promise 的 resolve/reject 本身具幂等性,早/晚两处调用互不冲突。 新增 2 个回归测试:browser 模式 onerror 主回调抛错、native 模式 xhr 阶段 失败后于 onloadend 内呼叫 abort()。均已验证在修复前会失败(前者断言失败, 后者测试超时挂起),修复后转为通过。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This comment was marked as outdated.
This comment was marked as outdated.
- xhr 阶段(ontimeout/onerror)改为在呼叫使用者回调前先清空 nativeAbort。 此前若使用者在 onloadend 内呼叫 download.abort(),会经由 nativeAbort() 无条件断开内部 GM_xmlhttpRequest 的消息连线;而该内部请求要到 details.onerror 返回后才会置位 reqDone,其 abort() 又只在提供了 onabort 回调时才合成 loadend——下载场景未提供 onabort,等于内部请求 永远收不到自己的 onloadend,其 retPromise 永久 pending,也无法完成 refCleanup。清空 nativeAbort 后,abort() 中的 nativeAbort?.() 变为 空操作,内部请求得以自然走完生命周期。 - xhr 阶段的使用者回调(含新增的 onloadend)现在被 try/catch 包裹: 例外不再同步向上传播回 GM_xmlhttpRequest 内部的消息处理循环——那里 在 code===-1 协议错误分支会在 details.onerror 返回后才安排合成 onloadend(因为该分支预期不会有真正的 onloadend 消息到达),一旦 同步抛出会跳过这个排程,同样导致内部请求永久卡住。改为透过 queueMicrotask 异步重新抛出,例外仍可被观察到,但不再阻断内部 生命周期收尾。 新增 1 个回归测试并强化既有 abort 竞态测试: - 验证 xhr 阶段失败后于 onloadend 内呼叫 abort(),不会再触发内部 GM_xmlhttpRequest 自身的 abort(即不中断其消息连线)。 - 验证 xhr 阶段使用者回调抛错时,不会同步向上传播,而是透过 queueMicrotask 异步重新抛出。 均已验证在修复前会失败(前者断言失败,后者同步抛出未被捕获), 修复后转为通过。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This comment was marked as outdated.
This comment was marked as outdated.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Checklist / 检查清单
背景
当前
GM.download/GM_download支持通过onload、onerror和ontimeout分别处理不同的下载终止状态,但缺少一个无论成功、失败还是超时都会执行的统一收尾回调。这使用户脚本不得不在多个终止回调中重复执行相同的清理、状态更新或 UI 收尾逻辑,也与
GM_xmlhttpRequest已提供的onloadend生命周期接口不一致。本次改动
为
GMTypes.DownloadDetails新增onloadend回调。同步更新以下类型声明:
src/types/scriptcat.d.tssrc/types/scriptcat.zh-CN.d.tssrc/template/scriptcat.d.tpl在以下下载路径中接入
onloadend:downloadMode: "browser":直接通过后台chrome.downloads下载;downloadMode: "native":先通过GM_xmlhttpRequest获取 Blob,再交由后台保存;在下载通过以下状态结束时,先执行对应主回调,再执行
onloadend:onloadonerrorontimeoutsave_cancelled,沿用现有兼容行为,将用户取消“另存为”视为成功。browser 和 native 最终保存阶段共用统一的后台消息处理,避免两条路径的终止语义出现差异。
browser 或 native 最终保存阶段连接后台失败时,也会依次执行
onerror、onloadend并 reject Promise,避免请求永久 pending。native XHR 阶段失败时直接完成外层回调和 Promise,并阻止后续部分 Blob 被继续保存。
补充单元和回归测试,覆盖成功、失败、超时、后台连接失败、未知后台消息、部分数据、回调抛错、主动中止竞态、Promise 收尾及 Blob URL 释放。
实现考虑
1. 保证主回调与
onloadend相互独立终止处理统一通过
withLoadEnd执行:onload、onerror或ontimeout;onloadend;这可以避免以下情况:
onload/onerror/ontimeout抛错后,onloadend被跳过;onloadend抛错后,GM.download返回的 Promise 永久 pending;2. 统一 browser 与 native 的最终下载阶段
browser 模式会直接连接后台执行下载;native 模式则分为两个阶段:
GM_xmlhttpRequest获取完整 Blob;两条路径最终都会收到相同的后台消息,例如:
onloadsave_cancelledonprogressontimeoutonerror因此共用同一套终止处理逻辑。
native 模式额外传入幂等的
releaseResources,在以下场景中释放 Blob URL:3. 后台连接失败也必须完整结束生命周期
browser 模式和 native 最终保存阶段都依赖与 Service Worker 建立消息连接。
如果
connect()直接 reject,而调用侧不主动处理:onerror不会执行;onloadend不会执行;GM.download返回的 Promise 会永久 pending;因此连接创建失败会按照普通下载错误处理:
onerror;onloadend;4. 避免将失败请求的部分数据保存为文件
native 模式下,底层 XHR 即使已经触发
onerror或ontimeout,后续onloadend仍可能携带由已接收数据组成的非空 Blob。如果仅判断
response instanceof Blob,这部分不完整数据可能被误认为下载成功,并继续交给chrome.downloads保存为截断文件,同时造成用户侧onloadend被重复触发。实现通过
xhrFailed标记记录 XHR 阶段是否已经失败。失败后收到的 XHRonloadend不再进入 Blob 保存流程。5. 避免
abort()破坏内部 XHR 的收尾流程native 模式的 XHR 阶段进入
onerror或ontimeout后,会在调用用户回调前清空nativeAbort。这样即使用户在
onloadend中调用外层download.abort(),也不会再次中止并断开内部GM_xmlhttpRequest的消息通道,避免:onloadend;外层
GM.download的 Promise 会直接在 XHR 失败回调的收尾阶段 reject,而不是仅依赖内部 XHR Promise 延迟返回失败结果。6. 用户回调异常不能中断内部状态机
在 native XHR 的
onerror/ontimeout阶段,用户回调抛出的异常不能同步传播回GM_xmlhttpRequest的消息处理循环。否则可能中断内部完成状态更新、合成
onloadend排程及引用清理。因此该路径会先完成:
onloadend;随后再通过微任务异步重新抛出用户回调异常,使异常仍可被全局错误处理捕获,但不会破坏内部状态机。
已知限制
1. 主动调用
abort()不会新增触发onloadend本次改动只为成功、失败、超时以及下载保存阶段的终止结果补充
onloadend。主动调用返回对象的
abort()仍沿用现有行为,不额外合成onabort或onloadend。如需统一主动中止的生命周期,需要单独设计
GM.download的中止事件语义。2.
onloadend参数沿用对应终止事件的数据onloadend不定义新的专用响应结构,而是尽量复用对应终止状态的数据:onload相同的下载结果;{ error: "unknown" };context。因此它不是原生 DOM
ProgressEvent的完整映射。3.
save_cancelled继续视为成功用户在“另存为”窗口中取消操作时,现有实现会将
save_cancelled视为成功,并依次调用onload和onloadend、resolveGM.downloadPromise。本 PR 保留该兼容行为,不调整取消保存的结果语义。
建议审查重点
onload/onerror/ontimeout与onloadend的调用顺序是否符合预期;xhrFailed是否能完整阻止部分 Blob 被继续保存;onloadend内调用abort()时,外层和内部 Promise 是否都能正常结束;