布局 flush 不再等待封面链
flushProjectPersistence 显式关掉 queueProjectLayoutSave 内建的 fire-and-forget 封面分支,自己另起一份并在最后 await。于是每个 await flush 的调用方都被挂在 封面链后面:封面渲染要为每个可绘制图层取 signed URL 再 new Image() 加载,而 那个 Image 只有 onload/onerror,没有 timeout 也没有 AbortSignal,外层的 try/catch 只接得住 reject、接不住「永不 settle」。一张图不 settle,生成 POST 就永远发不出去。 这是「完美像素请求账本移出项目布局」把严格通道与普通 flush 合并成一条路径时 引入的回归——改动前 strict 分支在封面链启动之前就 return,两者是结构性隔离的。 图集拆分一直走非严格路径,暴露是既有的,但同源,一并解开。 必然发生的是延迟:封面签名含未量化的 viewport,而创建占位时会移动视口,所以 几乎每次完美像素都触发全量重渲染,而这些与服务端那个 409 前置毫无关系。可能 发生的是挂死:此时 finally 永不执行,图层锁与归属登记被永久持有,用户删掉占位 也没用——闸的另一半是静默 return,再点没有任何反应。 删除式修复:去掉 persistCover: false 覆盖、独立的 coverSave 与末尾的 await, 让封面回到 queue 内建的 fire-and-forget 分支。参数逐项等价,封面照存,只是不再 有人等它。未采纳加选项的方案,那会把同一问题留在图集拆分身上。 代价是 returnToProjects 不再等封面就跳转;该保护本就很薄——SPA 跳转不会打断 promise,真正打断它的是页面卸载,而 await 在 beforeunload 里同样救不了。 遗留:loadProjectCoverImage 里无界的 new Image() 仍是隐患,自动保存路径一样会 踩,本次只是把它移出生成链的关键路径。建议单开。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -6563,3 +6563,14 @@
|
||||
- 测试缺口的根因:原用例的夹具 `durablePerfectPixelDialog` 带着 `perfectPixelOperation`,编码了与判据相同的错误假设,结构上不可能覆盖 operation 形成之前的窗口。夹具已补上 marker 以还原真实形状,并新增「只有 marker、尚无账本」的用例,已实证:回退判据后报 `expected true to be false`。
|
||||
- 影响范围:`useCanvasGenerationDialogs.ts` 的判据、`useImageCanvasGenerationWorkflow.ts` 的占位创建。不修改服务端、SpacetimeDB schema 或对外契约。
|
||||
- 关联文档:`docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md`。
|
||||
|
||||
## 2026-08-05 布局 flush 不再等待封面链
|
||||
|
||||
- 缺陷:`flushProjectPersistence` 显式关掉 `queueProjectLayoutSave` 内建的 fire-and-forget 封面分支(传 `persistCover: false`),自己另起一份并在函数最后 `await coverSave`。于是每一个 `await flushProjectPersistence(...)` 的调用方都被挂在封面链后面。封面渲染要为**每个可绘制图层**取 signed URL、再用 `new Image()` 加载——那个 Image 只有 `onload` / `onerror`,**没有 timeout、没有 AbortSignal**,外层的 `try { } catch { }` 只接得住 reject、接不住「永不 settle」。一张图不 settle,生成 POST 就永远发不出去。
|
||||
- 归因:这是 2026-08-05「完美像素请求账本移出项目布局」把严格通道与普通 flush 合并成一条路径时引入的**回归**。改动前 `if (requireSuccess) { …; await strictCompletion.promise; return; }` 在封面链启动之前就返回,封面与 pre-POST 路径是结构性隔离的。图集拆分(`void flushProjectPersistence().then(() => splitSelectedIconSpritesheet(layer))`)一直走非严格路径,因此它的暴露是既有的、不是本次引入;但两者同源,一并解开。
|
||||
- 影响面分级:**必然发生**的是延迟——封面签名含未量化的 `viewport.x/y/scale`,而完美像素创建占位时 `openPlacedCanvasGenerationDialog` 会 `setViewport(centerViewportOnPlacement(...))`,所以几乎每次调用都会触发全量重渲染(逐图层取 signed URL + 加载 + 渲染 + 上传 OSS + 登记资源),这些与服务端那个 409 前置毫无关系。**可能发生**的是永久挂死,此时 `snapSelectedLayerToPerfectPixels` 的 `finally` 永不执行,图层锁与 inline 占位归属登记被永久持有;用户即使删掉占位,闸的另一半 `perfectPixelLayerIdsRef.current.has(sourceLayer.id)` 仍为真且**静默 return**,本会话内再点完美像素不会有任何反应。图集拆分没有这层脏状态——它的锁在 `splitSelectedIconSpritesheet` 函数体内才取,flush 挂住时根本没被调用,表现只是「点击无反馈」。
|
||||
- 决策(删除式修复):删掉 flush 里的 `persistCover: false` 覆盖、独立的 `const coverSave = persistProjectCoverSnapshot(...)` 与末尾的 `await coverSave`,让封面回到 `queueProjectLayoutSave` 内建的 fire-and-forget 分支。参数逐项等价(`layoutInput.viewport` 即 `coverDisplayViewport`、同一个 `refs.layersRef.current`、flush 不传 `delayMs` 故走立即分支),**封面照存,只是不再有人等它**。未采纳「给 flush 加一个跳过封面的选项」:那会把同一个结构性问题留在图集拆分身上,并且多一个需要每个调用方正确设置的开关。
|
||||
- 代价:`returnToProjects` 不再等封面就跳转。该保护本就很薄——跳转是 SPA 路由切换而非页面卸载,fire-and-forget 的 promise 在同一 JS 上下文里会跑完;真正会打断它的是浏览器关闭/硬刷新,而 `await` 在 `beforeunload` 里同样救不了。实际损失只是「点返回后立刻关标签页」这个窄窗口里封面可能没传完,而封面是缩略图、下次任意保存会重新生成。
|
||||
- 遗留(建议单开,不在本次范围):`loadProjectCoverImage` 里无 timeout / 无 AbortSignal 的 `new Image()` 本身仍是隐患,自动保存路径一样会踩。本次只是把它移出生成链的关键路径,没有消除它。
|
||||
- 影响范围:`useImageCanvasProjectPersistence.ts` 的 `flushProjectPersistence`。不改服务端、不改契约。
|
||||
- 验证方式:既有用例「flush 等待封面缓存」翻转为「flush 不等封面、但封面链照常跑完并完成上传与资源登记」;新增「封面永不 settle 时 flush 仍返回」——用永不 resolve 的 blob 模拟 `new Image()` 不 settle,并断言 `createProjectCoverSnapshotBlob` 确实被调用过以防用例空过。已实证:回退修复后新用例报 `expected 'false' to be 'true'`。运行 `npx vitest run src/components/image-editor src/components/platform-entry src/services`(101 文件 / 1241 项)、`npm run typecheck`、`npm run lint:eslint`、`npm run check:encoding`。
|
||||
|
||||
Reference in New Issue
Block a user