@atlaskit/editor-plugin-collab-edit
Version:
Collab Edit plugin for @atlaskit/editor-core
91 lines (87 loc) • 3.96 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.preserveNodeIdentity = preserveNodeIdentity;
var _model = require("@atlaskit/editor-prosemirror/model");
var _expValEquals = require("@atlaskit/tmp-editor-statsig/exp-val-equals");
/**
* Walk two Fragments (old and new) and return a Fragment that reuses old node
* references wherever structurally equal (`node.eq()` returns true).
*
* This preserves referential identity (`===`) for unchanged subtrees, which is
* critical for ProseMirror's view reconciliation performance. The `preMatch`
* optimisation in `prosemirror-view/src/viewdesc.ts` uses `===` to fast-match
* nodes against existing view descriptors. When all nodes are fresh objects
* (e.g. after `Node.fromJSON` in `replaceDocument`), `preMatch` fails for
* everything, and the fallback scan in `syncToMarks` is limited to 3
* positions — causing mark wrappers to be destroyed and recreated when widget
* decorations (gap cursor, telepointers, block controls) shift indices beyond
* that window. The destroyed mark wrappers take their React nodeviews with
* them, causing visible flicker.
*
* By preserving identity here, we ensure `preMatch` succeeds for unchanged
* subtrees, preventing unnecessary mark wrapper destruction and React nodeview
* re-mounting.
*
* @param oldFragment - Fragment from the current editor state (holds existing node references)
* @param newFragment - Fragment parsed from incoming document (fresh node objects)
* @returns A Fragment that reuses old node references where possible
*
* @see https://hello.jira.atlassian.cloud/browse/EDITOR-5277
* @see https://hello.jira.atlassian.cloud/browse/EDITOR-4424
*/
function preserveNodeIdentity(oldFragment, newFragment) {
// Fast path: referentially identical — nothing to do
if (oldFragment === newFragment) {
return oldFragment;
}
// Fast path: structurally equal — reuse old fragment entirely.
// This covers the common SSR case where collab init sends the same doc.
if (oldFragment.eq(newFragment)) {
return oldFragment;
}
// Walk children position-by-position and preserve what we can
var oldCount = oldFragment.childCount;
var newCount = newFragment.childCount;
var changed = false;
var children = [];
for (var i = 0; i < newCount; i++) {
var newChild = newFragment.child(i);
if (i < oldCount) {
var oldChild = oldFragment.child(i);
if (oldChild === newChild) {
// Already referentially identical
children.push(newChild);
} else if (oldChild.eq(newChild)) {
// Structurally equal — reuse old reference (THE KEY OPERATION)
children.push(oldChild);
} else if (oldChild.type === newChild.type && oldChild.sameMarkup(newChild) && oldChild.content.childCount > 0 && newChild.content.childCount > 0 && (0, _expValEquals.expValEquals)('platform_editor_preserve_node_identity', 'isRecursive', true)) {
// Same type and markup but different content — recurse into children
var preservedContent = preserveNodeIdentity(oldChild.content, newChild.content);
if (preservedContent === oldChild.content) {
// All content was preserved — reuse old node entirely
children.push(oldChild);
} else {
// Partially changed — create node with preserved content
children.push(oldChild.copy(preservedContent));
changed = true;
}
} else {
// Completely different node — use new
children.push(newChild);
changed = true;
}
} else {
// New child beyond old count — use as-is (insertion)
children.push(newChild);
changed = true;
}
}
// If nothing changed and counts match, return old fragment directly
// (avoids creating a new Fragment object)
if (!changed && oldCount === newCount) {
return oldFragment;
}
return _model.Fragment.from(children);
}