fix(git-sync): merge git body into the live doc block-by-block (no clobber)

Supersedes the active-session "defer" guard with a real merge (review #5 —
"запись делать через мерж", not skip-while-editing).

writeBody no longer does delete-all + re-insert (which discarded a concurrent
editor's in-flight changes on every sync). It now diffs the live body against the
incoming git body at TOP-LEVEL BLOCK granularity (LCS over a canonical structural
serialization) and applies only the minimal inserts/deletes:
- a block a human is editing is left UNTOUCHED when git changed a DIFFERENT block;
- an unchanged resync is a complete 0-op write;
- Yjs CRDT-merges the minimal ops with concurrent edits.

New yjs-body-merge.ts (mergeXmlFragments + cloneXmlNode + diffBlocks) is pure-Yjs
and unit-tested with real Y.Docs (8 tests): identical->0 ops, edit-one-block keeps
the other block instances, append/delete keep neighbours, marks survive the
cross-doc clone. Crash-safety kept: the incoming doc is built before the
connection opens, so a transform failure can't empty the body.

Removed: the ActiveEditSessionError defer path and the now-unused
CollaborationGateway.getActiveEditorCount.

Honest limitation: this is a 2-way merge — for a block BOTH sides changed since the
last sync, git wins (no common ancestor to decide). A full 3-way merge would need
the last-synced base plumbed from the engine; the dominant cases (unchanged
resync, edits to different blocks) are now lossless.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
claude code agent 227
2026-06-23 15:21:14 +03:00
parent 527fcb2be9
commit 5c178e95c9
5 changed files with 350 additions and 90 deletions
@@ -137,21 +137,6 @@ export class CollaborationGateway {
return this.hocuspocus.getDocumentsCount();
}
/**
* Number of LIVE human editor sessions (websocket connections) currently open
* on a document, or 0 if the document is not loaded. Unlike
* `Document.getConnectionsCount()` this deliberately excludes server-side
* direct connections (`directConnectionsCount`, e.g. the git-sync writer
* itself), so callers can tell whether a real person is editing right now.
*
* NOTE: this reflects only THIS instance. In a Redis-clustered deployment an
* editor attached to another node is not counted; for the single-instance
* deployments this guards (git-sync) that is exactly the live set.
*/
getActiveEditorCount(documentName: string): number {
return this.hocuspocus.documents.get(documentName)?.connections.size ?? 0;
}
handleYjsEvent<TName extends keyof CollabEventHandlers>(
eventName: TName,
documentName: string,